raider_reporter 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +31 -1
- data/exe/raider-reporter +8 -0
- data/lib/raider_reporter/ci_detector.rb +59 -3
- data/lib/raider_reporter/cli.rb +109 -0
- data/lib/raider_reporter/comment/github_client.rb +112 -0
- data/lib/raider_reporter/comment/summary_client.rb +74 -0
- data/lib/raider_reporter/github_event.rb +19 -0
- data/lib/raider_reporter/rspec/formatter.rb +1 -1
- data/lib/raider_reporter/version.rb +1 -1
- data/lib/raider_reporter.rb +3 -1
- metadata +11 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c4d38649c7323d93145079d6de1aac3a00f4724a0d906a2f004ddf331e201b77
|
|
4
|
+
data.tar.gz: 8f5ad9717d74c2c626ba0ab7507e2ebc413f9f80337d0ecfd02343c69bc52051
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d43a79f1ff81eff80f4fa8a9afbff388df98671e65920013b88cdb8cdd24ab3313266646e27e72bbebfbad4213231855c6d7470c28177ba601d0db1ddb1aea76
|
|
7
|
+
data.tar.gz: 61cd6025ab3d9c713fb075d5288eec3ce46c219eb317995129a6bc41fa139ecccf4026eca160cafeeeae0f25f8d4da290c9e661429cb1f21e8ad8520da007c94
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0 — 2026-10-11
|
|
4
|
+
|
|
5
|
+
- Report pull request identity: `run.ci.repository`, `run.git.default_branch` and `run.git.pr` (number, base/head branch, head SHA) on `pull_request` and `pull_request_target` events. Requires a Raider server that accepts `git.pr`
|
|
6
|
+
- Add `raider-reporter comment`: posts (and updates) one results comment on the pull request and sets the `raider/tests` commit status; fail-open, always exits 0, skips fork PRs
|
|
7
|
+
- Print the `Raider report: <url>` line to stderr instead of stdout, so stdout-parsing tools (such as `--format json`) stay clean
|
|
8
|
+
- Report the PR head commit's SHA and message on pull request events instead of the merge commit's
|
|
9
|
+
- Harden the GitHub client: only a bot-authored comment starting with the marker is updated, any number of trailing slashes in `GITHUB_API_URL` are accepted, and the token is redacted from error messages
|
|
10
|
+
|
|
3
11
|
## 0.2.0 — 2026-10-11
|
|
4
12
|
|
|
5
13
|
- Report the CI build key, attempt and job so runs of one build are grouped (GitHub Actions)
|
data/README.md
CHANGED
|
@@ -84,7 +84,7 @@ If both a token and an endpoint are set, the run (plus attachments) is uploaded
|
|
|
84
84
|
RAIDER_TOKEN=... RAIDER_ENDPOINT=https://your-raider-server bundle exec rspec
|
|
85
85
|
```
|
|
86
86
|
|
|
87
|
-
On success the formatter prints `Raider report: <url
|
|
87
|
+
On success the formatter prints `Raider report: <url>` to stderr (so tools that parse RSpec's stdout, such as `--format json`, stay clean), using the build URL when the server returns one and the run URL otherwise (or `Raider report uploaded (run <id>)` if the response has no URL). If either value is missing, nothing is uploaded and the local files are still written.
|
|
88
88
|
|
|
89
89
|
An upload carries at most 45 MB of attachments and at most 1000 attachment files. Attachments past that budget are left out of the upload (in order, with one warning) but stay in the local `raider-results/`.
|
|
90
90
|
|
|
@@ -92,6 +92,36 @@ Reporting is fail-open: no error inside the gem, network failure, timeout or non
|
|
|
92
92
|
|
|
93
93
|
The hosted Raider service is not public yet; uploads go to a Raider server whose address you provide in `RAIDER_ENDPOINT`.
|
|
94
94
|
|
|
95
|
+
## Pull request comments
|
|
96
|
+
|
|
97
|
+
On pull requests, `raider-reporter comment` posts one comment with the build's results (and sets a `raider/tests` commit status on the PR head commit). Run it in a job that runs after your test jobs, so every shard has uploaded. Use the workflow's `GITHUB_TOKEN` (or a GitHub App token): a personal access token posts as a user, so the step adds a new comment on every run instead of updating the existing one.
|
|
98
|
+
|
|
99
|
+
```yaml
|
|
100
|
+
raider-comment:
|
|
101
|
+
needs: [test] # every test job id
|
|
102
|
+
if: always() && github.event_name == 'pull_request'
|
|
103
|
+
runs-on: ubuntu-latest
|
|
104
|
+
permissions:
|
|
105
|
+
contents: read
|
|
106
|
+
pull-requests: write
|
|
107
|
+
statuses: write
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/checkout@v4
|
|
110
|
+
- uses: ruby/setup-ruby@v1
|
|
111
|
+
with: { ruby-version: "3.4", bundler-cache: true }
|
|
112
|
+
- name: Comment Raider results on the pull request
|
|
113
|
+
run: bundle exec raider-reporter comment
|
|
114
|
+
env:
|
|
115
|
+
RAIDER_TOKEN: ${{ secrets.RAIDER_TOKEN }}
|
|
116
|
+
RAIDER_ENDPOINT: ${{ vars.RAIDER_ENDPOINT }}
|
|
117
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
- The comment is rendered by the Raider server (`GET /api/v1/builds/summary`); the gem only fetches the Markdown and posts it. The same comment is updated on every run instead of adding a new one.
|
|
121
|
+
- Pull requests from forks are skipped: GitHub gives their workflows a read-only token, so the command prints a notice and exits 0.
|
|
122
|
+
- 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.
|
|
123
|
+
- It requires a Raider server that supports the summary endpoint and the `git.pr` field. Upgrade the server before using it.
|
|
124
|
+
|
|
95
125
|
## Parallel runs
|
|
96
126
|
|
|
97
127
|
When a suite is split across several processes or CI jobs, each one uploads its own run. Raider groups runs of the same CI build, so the shards show up together as one build. Each run records its shard (`index`, `total` and a `label`) and, on GitHub Actions, a build key made from the repository, run id and run attempt. A re-run attempt of a workflow is a separate build.
|
data/exe/raider-reporter
ADDED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "open3"
|
|
4
|
+
require "raider_reporter/github_event"
|
|
4
5
|
|
|
5
6
|
module RaiderReporter
|
|
6
7
|
class CiDetector
|
|
7
|
-
|
|
8
|
+
PR_EVENTS = %w[pull_request pull_request_target].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(env: ENV, git: method(:git_command), event_reader: nil)
|
|
8
11
|
@env = env
|
|
9
12
|
@git = git
|
|
13
|
+
@event_reader = event_reader || -> { GithubEvent.load(@env) }
|
|
10
14
|
end
|
|
11
15
|
|
|
12
16
|
def ci
|
|
@@ -20,17 +24,69 @@ module RaiderReporter
|
|
|
20
24
|
repo = @env["GITHUB_REPOSITORY"]
|
|
21
25
|
build_key = ("github_actions:#{repo}:#{run_id}:#{attempt}" if repo && run_id)
|
|
22
26
|
{ "provider" => "github_actions", "build_url" => build_url, "build_id" => run_id,
|
|
23
|
-
"attempt" => attempt, "job" => @env["GITHUB_JOB"], "build_key" => build_key
|
|
27
|
+
"attempt" => attempt, "job" => @env["GITHUB_JOB"], "build_key" => build_key,
|
|
28
|
+
"repository" => (repo unless repo.to_s.empty?) }.compact
|
|
24
29
|
end
|
|
25
30
|
|
|
26
31
|
def git
|
|
27
|
-
|
|
32
|
+
pr = pull_request
|
|
33
|
+
info = { "branch" => branch, "commit_sha" => pr ? pr["head_sha"] : sha, "commit_message" => commit_message(pr),
|
|
34
|
+
"default_branch" => default_branch, "pr" => pr }
|
|
28
35
|
info = info.compact
|
|
29
36
|
info.empty? ? nil : info
|
|
30
37
|
end
|
|
31
38
|
|
|
32
39
|
private
|
|
33
40
|
|
|
41
|
+
def event
|
|
42
|
+
return @event if defined?(@event)
|
|
43
|
+
|
|
44
|
+
loaded = begin
|
|
45
|
+
@event_reader.call
|
|
46
|
+
rescue StandardError
|
|
47
|
+
nil
|
|
48
|
+
end
|
|
49
|
+
@event = loaded.is_a?(Hash) ? loaded : nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def default_branch
|
|
53
|
+
value = event&.dig("repository", "default_branch")
|
|
54
|
+
value.is_a?(String) && !value.empty? ? value : nil
|
|
55
|
+
rescue StandardError
|
|
56
|
+
nil
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def pull_request
|
|
60
|
+
return nil unless event && PR_EVENTS.include?(@env["GITHUB_EVENT_NAME"])
|
|
61
|
+
|
|
62
|
+
pr = event["pull_request"]
|
|
63
|
+
return nil unless pr.is_a?(Hash)
|
|
64
|
+
|
|
65
|
+
number = pr["number"] || event["number"]
|
|
66
|
+
base = pr.dig("base", "ref")
|
|
67
|
+
head = pr.dig("head", "ref")
|
|
68
|
+
sha = pr.dig("head", "sha")
|
|
69
|
+
return nil unless number.is_a?(Integer) && number >= 1
|
|
70
|
+
return nil unless [base, head, sha].all? { |v| v.is_a?(String) && !v.empty? }
|
|
71
|
+
|
|
72
|
+
{ "number" => number, "base_branch" => base, "head_branch" => head, "head_sha" => sha }
|
|
73
|
+
rescue StandardError
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# On a PR, GitHub checks out a synthetic merge commit; its message is not the author's.
|
|
78
|
+
def commit_message(pr)
|
|
79
|
+
if pr && merge_commit?
|
|
80
|
+
message = @git.call("log", "-1", "--format=%s", "HEAD^2")
|
|
81
|
+
return message unless message.to_s.empty?
|
|
82
|
+
end
|
|
83
|
+
@git.call("log", "-1", "--pretty=%s")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def merge_commit?
|
|
87
|
+
@git.call("rev-list", "--parents", "-n", "1", "HEAD").to_s.split.size == 3
|
|
88
|
+
end
|
|
89
|
+
|
|
34
90
|
def branch
|
|
35
91
|
head_ref = @env["GITHUB_HEAD_REF"].to_s
|
|
36
92
|
return head_ref unless head_ref.empty?
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "raider_reporter"
|
|
4
|
+
|
|
5
|
+
module RaiderReporter
|
|
6
|
+
# `raider-reporter comment`: posts the build summary as a PR comment and sets a commit status.
|
|
7
|
+
# Fail-open: #run always returns 0, notices go to stderr, nothing goes to stdout.
|
|
8
|
+
class CLI
|
|
9
|
+
MARKER = "<!-- raider-reporter:build-comment -->"
|
|
10
|
+
CONTEXT = "raider/tests"
|
|
11
|
+
REQUIRED_ENV = %w[GITHUB_TOKEN GITHUB_REPOSITORY GITHUB_EVENT_PATH RAIDER_TOKEN RAIDER_ENDPOINT].freeze
|
|
12
|
+
STATUS_STATES = { "passed" => "success", "failed" => "failure", "incomplete" => "pending" }.freeze
|
|
13
|
+
FALLBACK_DESCRIPTIONS = {
|
|
14
|
+
"passed" => "Raider: tests passed",
|
|
15
|
+
"failed" => "Raider: tests failed",
|
|
16
|
+
"incomplete" => "Raider: build incomplete"
|
|
17
|
+
}.freeze
|
|
18
|
+
DEFAULT_API_URL = "https://api.github.com"
|
|
19
|
+
USAGE = <<~TEXT
|
|
20
|
+
Usage: raider-reporter comment
|
|
21
|
+
|
|
22
|
+
Posts (or updates) the Raider build summary as a comment on the current pull request
|
|
23
|
+
and sets the "#{CONTEXT}" commit status. Intended for GitHub Actions; always exits 0.
|
|
24
|
+
|
|
25
|
+
Environment: #{REQUIRED_ENV.join(', ')} (GITHUB_API_URL optional).
|
|
26
|
+
TEXT
|
|
27
|
+
|
|
28
|
+
def initialize(argv, env: ENV, out: $stdout, err: $stderr)
|
|
29
|
+
@argv = argv
|
|
30
|
+
@env = env
|
|
31
|
+
@out = out
|
|
32
|
+
@err = err
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def run
|
|
36
|
+
command = @argv.first
|
|
37
|
+
if command == "comment" && !@argv.include?("--help") && !@argv.include?("-h")
|
|
38
|
+
comment
|
|
39
|
+
else
|
|
40
|
+
@err.puts(USAGE)
|
|
41
|
+
end
|
|
42
|
+
0
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
notice("comment failed: #{e.class}: #{e.message}")
|
|
45
|
+
0
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def comment
|
|
51
|
+
missing = REQUIRED_ENV.select { |name| @env[name].to_s.strip.empty? }
|
|
52
|
+
return notice("skipped: missing #{missing.join(', ')}") unless missing.empty?
|
|
53
|
+
|
|
54
|
+
event = GithubEvent.load(@env)
|
|
55
|
+
detector = CiDetector.new(env: @env, event_reader: -> { event })
|
|
56
|
+
pr = detector.git&.dig("pr")
|
|
57
|
+
return notice("skipped: not a pull request event") unless pr
|
|
58
|
+
|
|
59
|
+
build_key = detector.ci&.dig("build_key")
|
|
60
|
+
return notice("skipped: could not determine the build key") unless build_key
|
|
61
|
+
|
|
62
|
+
publish(pr, build_key)
|
|
63
|
+
rescue Comment::GithubClient::Forbidden
|
|
64
|
+
notice("skipped: token cannot write to this PR")
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
notice("comment failed: #{e.class}: #{e.message}")
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def publish(pr, build_key)
|
|
70
|
+
summaries = Comment::SummaryClient.new(endpoint: @env["RAIDER_ENDPOINT"], token: @env["RAIDER_TOKEN"])
|
|
71
|
+
summary = summaries.fetch(build_key)
|
|
72
|
+
return notice("summary unavailable: #{summaries.last_error}") unless summary
|
|
73
|
+
|
|
74
|
+
github = Comment::GithubClient.new(token: @env["GITHUB_TOKEN"], repository: @env["GITHUB_REPOSITORY"],
|
|
75
|
+
api_url: api_url)
|
|
76
|
+
html_url = github.upsert_comment(pr["number"], summary.markdown, MARKER)
|
|
77
|
+
github.set_status(pr["head_sha"], state: STATUS_STATES.fetch(summary.state, "pending"),
|
|
78
|
+
target_url: summary.url, description: description(summary),
|
|
79
|
+
context: CONTEXT)
|
|
80
|
+
notice("PR comment updated: #{html_url}")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def api_url
|
|
84
|
+
url = @env["GITHUB_API_URL"].to_s.strip
|
|
85
|
+
url.empty? ? DEFAULT_API_URL : url
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# "**Raider: failed** - 2 failed, 148 passed, 3 skipped (1m 12s)" -> "2 failed, 148 passed, 3 skipped".
|
|
89
|
+
# Falls back to a fixed text per state when the Markdown carries no headline counts.
|
|
90
|
+
def description(summary)
|
|
91
|
+
counts = headline_counts(summary.markdown)
|
|
92
|
+
text = counts || FALLBACK_DESCRIPTIONS.fetch(summary.state, FALLBACK_DESCRIPTIONS["incomplete"])
|
|
93
|
+
text[0, Comment::GithubClient::DESCRIPTION_LIMIT]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def headline_counts(markdown)
|
|
97
|
+
line = markdown.to_s.lines.find { |l| l.include?("Raider:") }
|
|
98
|
+
return nil unless line
|
|
99
|
+
|
|
100
|
+
counts = line.scan(/\b\d+ (?:failed|passed|skipped|pending)\b/)
|
|
101
|
+
counts.empty? ? nil : counts.join(", ")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def notice(message)
|
|
105
|
+
@err.puts("[raider_reporter] #{message}")
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
|
|
7
|
+
module RaiderReporter
|
|
8
|
+
module Comment
|
|
9
|
+
# Minimal GitHub REST client: upsert one marked PR comment and set a commit status.
|
|
10
|
+
class GithubClient
|
|
11
|
+
class Error < StandardError; end
|
|
12
|
+
# 403/404: the token cannot write here (e.g. a fork's read-only token).
|
|
13
|
+
class Forbidden < Error; end
|
|
14
|
+
|
|
15
|
+
MAX_PAGES = 10
|
|
16
|
+
PER_PAGE = 100
|
|
17
|
+
DESCRIPTION_LIMIT = 140
|
|
18
|
+
|
|
19
|
+
def initialize(token:, repository:, api_url: "https://api.github.com", timeout: 30)
|
|
20
|
+
@token = token
|
|
21
|
+
@repository = repository
|
|
22
|
+
@api_url = api_url.to_s.strip.sub(%r{/+\z}, "")
|
|
23
|
+
@timeout = timeout
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def upsert_comment(pr_number, body, marker)
|
|
27
|
+
existing = find_comment(pr_number, marker)
|
|
28
|
+
response = if existing
|
|
29
|
+
request(:patch, "/repos/#{@repository}/issues/comments/#{existing['id']}", body: body)
|
|
30
|
+
else
|
|
31
|
+
request(:post, "/repos/#{@repository}/issues/#{pr_number}/comments", body: body)
|
|
32
|
+
end
|
|
33
|
+
html_url = response.is_a?(Hash) ? response["html_url"] : nil
|
|
34
|
+
raise Error, "GitHub response had no html_url" if html_url.to_s.empty?
|
|
35
|
+
|
|
36
|
+
html_url
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def set_status(sha, state:, target_url:, description:, context:)
|
|
40
|
+
return nil if sha.to_s.strip.empty?
|
|
41
|
+
|
|
42
|
+
payload = { state: state, target_url: target_url, context: context,
|
|
43
|
+
description: description.to_s[0, DESCRIPTION_LIMIT] }
|
|
44
|
+
request(:post, "/repos/#{@repository}/statuses/#{sha}", payload: payload)
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def find_comment(pr_number, marker)
|
|
51
|
+
(1..MAX_PAGES).each do |page|
|
|
52
|
+
comments = request(:get, "/repos/#{@repository}/issues/#{pr_number}/comments?per_page=#{PER_PAGE}&page=#{page}")
|
|
53
|
+
raise Error, "unexpected comments response" unless comments.is_a?(Array)
|
|
54
|
+
|
|
55
|
+
found = comments.find { |c| ours?(c, marker) }
|
|
56
|
+
return found if found
|
|
57
|
+
return nil if comments.size < PER_PAGE
|
|
58
|
+
end
|
|
59
|
+
nil
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Only our own (bot-authored) marked comment is ever edited.
|
|
63
|
+
def ours?(comment, marker)
|
|
64
|
+
comment.is_a?(Hash) && comment["body"].to_s.start_with?(marker) &&
|
|
65
|
+
comment.dig("user", "type") == "Bot"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def redact(text)
|
|
69
|
+
@token.to_s.empty? ? text.to_s : text.to_s.gsub(@token.to_s, "[REDACTED]")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def request(verb, path, body: nil, payload: nil)
|
|
73
|
+
uri = URI.parse(@api_url + path)
|
|
74
|
+
raise Error, "GITHUB_API_URL must start with http:// or https://" unless uri.is_a?(URI::HTTP)
|
|
75
|
+
|
|
76
|
+
req = build_request(verb, uri, payload || ({ body: body } if body))
|
|
77
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
78
|
+
open_timeout: @timeout, read_timeout: @timeout,
|
|
79
|
+
write_timeout: @timeout) { |http| http.request(req) }
|
|
80
|
+
handle(response)
|
|
81
|
+
rescue Error
|
|
82
|
+
raise
|
|
83
|
+
rescue StandardError => e
|
|
84
|
+
raise Error, redact("#{e.class}: #{e.message}")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def build_request(verb, uri, json)
|
|
88
|
+
klass = { get: Net::HTTP::Get, post: Net::HTTP::Post, patch: Net::HTTP::Patch }.fetch(verb)
|
|
89
|
+
req = klass.new(uri)
|
|
90
|
+
req["Authorization"] = "Bearer #{@token}"
|
|
91
|
+
req["Accept"] = "application/vnd.github+json"
|
|
92
|
+
req["X-GitHub-Api-Version"] = "2022-11-28"
|
|
93
|
+
req["User-Agent"] = "raider_reporter/#{VERSION}"
|
|
94
|
+
if json
|
|
95
|
+
req["Content-Type"] = "application/json"
|
|
96
|
+
req.body = JSON.generate(json)
|
|
97
|
+
end
|
|
98
|
+
req
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def handle(response)
|
|
102
|
+
code = response.code.to_i
|
|
103
|
+
raise Forbidden, "HTTP #{code}" if [403, 404].include?(code)
|
|
104
|
+
raise Error, redact("HTTP #{code}: #{response.body.to_s[0, 200]}") unless (200..299).cover?(code)
|
|
105
|
+
|
|
106
|
+
JSON.parse(response.body.to_s)
|
|
107
|
+
rescue JSON::ParserError
|
|
108
|
+
raise Error, "GitHub returned invalid JSON"
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "uri"
|
|
5
|
+
require "json"
|
|
6
|
+
require "cgi/escape"
|
|
7
|
+
|
|
8
|
+
module RaiderReporter
|
|
9
|
+
module Comment
|
|
10
|
+
# Fetches the rendered build summary from the Raider API. Never raises:
|
|
11
|
+
# failures return nil and set #last_error to a printable reason.
|
|
12
|
+
class SummaryClient
|
|
13
|
+
Summary = Data.define(:state, :markdown, :url)
|
|
14
|
+
|
|
15
|
+
attr_reader :last_error
|
|
16
|
+
|
|
17
|
+
def initialize(endpoint:, token:, timeout: 30)
|
|
18
|
+
@endpoint = endpoint
|
|
19
|
+
@token = token
|
|
20
|
+
@timeout = timeout
|
|
21
|
+
@last_error = nil
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def fetch(build_key)
|
|
25
|
+
@last_error = nil
|
|
26
|
+
uri = summary_uri(build_key)
|
|
27
|
+
request = Net::HTTP::Get.new(uri)
|
|
28
|
+
request["Authorization"] = "Bearer #{@token}"
|
|
29
|
+
request["Accept"] = "application/json"
|
|
30
|
+
request["User-Agent"] = "raider_reporter/#{VERSION}"
|
|
31
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
32
|
+
open_timeout: @timeout, read_timeout: @timeout,
|
|
33
|
+
write_timeout: @timeout) { |http| http.request(request) }
|
|
34
|
+
parse(response)
|
|
35
|
+
rescue StandardError => e
|
|
36
|
+
fail_with("#{e.class}: #{e.message}")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def summary_uri(build_key)
|
|
42
|
+
uri = URI.parse("#{@endpoint.to_s.strip.chomp('/')}/api/v1/builds/summary?build_key=#{CGI.escape(build_key.to_s)}")
|
|
43
|
+
raise ArgumentError, "endpoint must start with http:// or https://" unless uri.is_a?(URI::HTTP)
|
|
44
|
+
|
|
45
|
+
uri
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse(response)
|
|
49
|
+
case response.code.to_i
|
|
50
|
+
when 200..299 then build_summary(response.body)
|
|
51
|
+
when 401 then fail_with("HTTP 401: unauthorized (check RAIDER_TOKEN)")
|
|
52
|
+
when 404 then fail_with("HTTP 404: build not found for this project")
|
|
53
|
+
else fail_with("HTTP #{response.code}: #{response.body.to_s[0, 200]}")
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def build_summary(text)
|
|
58
|
+
data = JSON.parse(text.to_s)
|
|
59
|
+
unless data.is_a?(Hash) && %w[state markdown url].all? { |k| data[k].is_a?(String) }
|
|
60
|
+
return fail_with("invalid summary response")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
Summary.new(state: data["state"], markdown: data["markdown"], url: data["url"])
|
|
64
|
+
rescue JSON::ParserError
|
|
65
|
+
fail_with("invalid summary response (not JSON)")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fail_with(reason)
|
|
69
|
+
@last_error = @token.to_s.empty? ? reason : reason.gsub(@token.to_s, "[REDACTED]")
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RaiderReporter
|
|
6
|
+
# Reads the webhook payload GitHub Actions leaves at GITHUB_EVENT_PATH.
|
|
7
|
+
module GithubEvent
|
|
8
|
+
# Returns the parsed event Hash, or nil when missing, unreadable or invalid.
|
|
9
|
+
def self.load(env = ENV)
|
|
10
|
+
path = env["GITHUB_EVENT_PATH"].to_s
|
|
11
|
+
return nil if path.empty?
|
|
12
|
+
|
|
13
|
+
data = JSON.parse(File.read(path))
|
|
14
|
+
data.is_a?(Hash) ? data : nil
|
|
15
|
+
rescue StandardError
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/raider_reporter.rb
CHANGED
|
@@ -12,6 +12,8 @@ require_relative "raider_reporter/writers/local"
|
|
|
12
12
|
require_relative "raider_reporter/ci_detector"
|
|
13
13
|
require_relative "raider_reporter/shard_detector"
|
|
14
14
|
require_relative "raider_reporter/writers/uploader"
|
|
15
|
+
require_relative "raider_reporter/comment/summary_client"
|
|
16
|
+
require_relative "raider_reporter/comment/github_client"
|
|
15
17
|
|
|
16
18
|
module RaiderReporter
|
|
17
19
|
class << self
|
|
@@ -66,7 +68,7 @@ module RaiderReporter
|
|
|
66
68
|
$stderr.puts("[raider_reporter] #{message}")
|
|
67
69
|
end
|
|
68
70
|
|
|
69
|
-
def finish!(output = $
|
|
71
|
+
def finish!(output = $stderr)
|
|
70
72
|
return nil if dry_run?
|
|
71
73
|
|
|
72
74
|
detector = CiDetector.new
|
metadata
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: raider_reporter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Augustin Gottlieb
|
|
8
|
-
bindir:
|
|
8
|
+
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date:
|
|
10
|
+
date: 2026-10-11 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
12
|
description: RSpec formatter that records results, screenshots and videos in the Raider
|
|
13
13
|
schema, writes them locally, and optionally uploads them to a Raider server.
|
|
14
14
|
email:
|
|
15
15
|
- 33221555+aguspe@users.noreply.github.com
|
|
16
|
-
executables:
|
|
16
|
+
executables:
|
|
17
|
+
- raider-reporter
|
|
17
18
|
extensions: []
|
|
18
19
|
extra_rdoc_files: []
|
|
19
20
|
files:
|
|
20
21
|
- CHANGELOG.md
|
|
21
22
|
- LICENSE
|
|
22
23
|
- README.md
|
|
24
|
+
- exe/raider-reporter
|
|
23
25
|
- lib/raider_reporter.rb
|
|
24
26
|
- lib/raider_reporter/ci_detector.rb
|
|
27
|
+
- lib/raider_reporter/cli.rb
|
|
25
28
|
- lib/raider_reporter/collector.rb
|
|
29
|
+
- lib/raider_reporter/comment/github_client.rb
|
|
30
|
+
- lib/raider_reporter/comment/summary_client.rb
|
|
26
31
|
- lib/raider_reporter/configuration.rb
|
|
27
32
|
- lib/raider_reporter/content_types.rb
|
|
33
|
+
- lib/raider_reporter/github_event.rb
|
|
28
34
|
- lib/raider_reporter/models.rb
|
|
29
35
|
- lib/raider_reporter/payload.rb
|
|
30
36
|
- lib/raider_reporter/rspec.rb
|
|
@@ -54,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
54
60
|
- !ruby/object:Gem::Version
|
|
55
61
|
version: '0'
|
|
56
62
|
requirements: []
|
|
57
|
-
rubygems_version:
|
|
63
|
+
rubygems_version: 3.6.2
|
|
58
64
|
specification_version: 4
|
|
59
65
|
summary: 'Test reporting for Ruby Raider: local run.json and Raider uploads'
|
|
60
66
|
test_files: []
|