raider_reporter 0.1.1 → 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 +15 -0
- data/README.md +91 -3
- data/exe/raider-reporter +8 -0
- data/lib/raider_reporter/ci_detector.rb +63 -3
- data/lib/raider_reporter/cli.rb +109 -0
- data/lib/raider_reporter/collector.rb +2 -2
- 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/models.rb +1 -1
- data/lib/raider_reporter/payload.rb +9 -2
- data/lib/raider_reporter/rspec/formatter.rb +1 -1
- data/lib/raider_reporter/shard_detector.rb +49 -0
- data/lib/raider_reporter/version.rb +1 -1
- data/lib/raider_reporter/writers/local.rb +6 -6
- data/lib/raider_reporter/writers/uploader.rb +6 -4
- data/lib/raider_reporter.rb +9 -3
- metadata +12 -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,20 @@
|
|
|
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
|
+
|
|
11
|
+
## 0.2.0 — 2026-10-11
|
|
12
|
+
|
|
13
|
+
- Report the CI build key, attempt and job so runs of one build are grouped (GitHub Actions)
|
|
14
|
+
- Detect shards from `RAIDER_SHARD_INDEX`/`RAIDER_SHARD_TOTAL` or `parallel_tests` (`TEST_ENV_NUMBER`, `PARALLEL_TEST_GROUPS`) and report them per run; a matrix leg running parallel_tests is composed into unique shards
|
|
15
|
+
- Write parallel-safe local output (`run-<n>.json`, `attachments-<n>/`) when sharded; fixes parallel_tests processes overwriting each other's results
|
|
16
|
+
- Print the Raider build URL after upload, falling back to the run URL
|
|
17
|
+
|
|
3
18
|
## 0.1.1 — 2026-10-11
|
|
4
19
|
|
|
5
20
|
- Send run timestamps with millisecond precision, so durations under a second are no longer reported as 0
|
data/README.md
CHANGED
|
@@ -63,7 +63,7 @@ end
|
|
|
63
63
|
| `token` | `ENV["RAIDER_TOKEN"]` | API token used for uploads. |
|
|
64
64
|
| `endpoint` | `ENV["RAIDER_ENDPOINT"]` | Base URL of the Raider server to upload to. |
|
|
65
65
|
| `output_dir` | `"raider-results"` | Directory for local output. |
|
|
66
|
-
| `clean_output_dir` | `true` | Remove the previous `run.json` and `attachments/` directory inside `output_dir` before writing. Nothing else in `output_dir` is touched. |
|
|
66
|
+
| `clean_output_dir` | `true` | Remove the previous `run.json` and `attachments/` directory (`run-<n>.json` and `attachments-<n>/` when sharded) inside `output_dir` before writing. Nothing else in `output_dir` is touched. |
|
|
67
67
|
| `environment` | `{}` | Free-form key/values stored with the run (browser, suite name, and so on). Values are JSON values: strings, booleans or numbers. |
|
|
68
68
|
|
|
69
69
|
## Local output
|
|
@@ -74,7 +74,7 @@ raider-results/
|
|
|
74
74
|
attachments/ # copies of attached screenshots and videos
|
|
75
75
|
```
|
|
76
76
|
|
|
77
|
-
Add `raider-results/` to your `.gitignore`.
|
|
77
|
+
Add `raider-results/` to your `.gitignore`. When a run is sharded, the files are named per shard; see [Parallel runs](#parallel-runs).
|
|
78
78
|
|
|
79
79
|
## Uploading to Raider
|
|
80
80
|
|
|
@@ -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>` (or `Raider report uploaded (run <id>)` if the
|
|
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,94 @@ 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
|
+
|
|
125
|
+
## Parallel runs
|
|
126
|
+
|
|
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.
|
|
128
|
+
|
|
129
|
+
Shard detection, in order of precedence:
|
|
130
|
+
|
|
131
|
+
1. `RAIDER_SHARD_INDEX` (1 or more) and, optionally, `RAIDER_SHARD_TOTAL`.
|
|
132
|
+
2. `parallel_tests` / `parallel_rspec`: `TEST_ENV_NUMBER` (empty means shard 1) and `PARALLEL_TEST_GROUPS` for the total.
|
|
133
|
+
3. Otherwise the run is not sharded.
|
|
134
|
+
|
|
135
|
+
A GitHub Actions matrix needs only the two variables:
|
|
136
|
+
|
|
137
|
+
```yaml
|
|
138
|
+
jobs:
|
|
139
|
+
test:
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
strategy:
|
|
142
|
+
matrix:
|
|
143
|
+
shard: [1, 2, 3, 4]
|
|
144
|
+
steps:
|
|
145
|
+
- uses: actions/checkout@v4
|
|
146
|
+
- uses: ruby/setup-ruby@v1
|
|
147
|
+
with:
|
|
148
|
+
bundler-cache: true
|
|
149
|
+
- run: bundle exec rspec # split the spec files by shard however you like
|
|
150
|
+
env:
|
|
151
|
+
RAIDER_TOKEN: ${{ secrets.RAIDER_TOKEN }}
|
|
152
|
+
RAIDER_ENDPOINT: ${{ vars.RAIDER_ENDPOINT }}
|
|
153
|
+
RAIDER_SHARD_INDEX: ${{ matrix.shard }}
|
|
154
|
+
RAIDER_SHARD_TOTAL: 4
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`parallel_rspec` works with no configuration, because it sets `TEST_ENV_NUMBER` and `PARALLEL_TEST_GROUPS` for each process.
|
|
158
|
+
|
|
159
|
+
When a run is sharded, local output is written per shard so that processes sharing one `raider-results/` do not overwrite each other. Cleaning only removes the current shard's own files:
|
|
160
|
+
|
|
161
|
+
```
|
|
162
|
+
raider-results/
|
|
163
|
+
run-1.json
|
|
164
|
+
attachments-1/
|
|
165
|
+
run-2.json
|
|
166
|
+
attachments-2/
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Unsharded runs still write `run.json` and `attachments/`. Tools that read local results should load `run*.json`.
|
|
170
|
+
|
|
171
|
+
### Matrix and parallel_tests together
|
|
172
|
+
|
|
173
|
+
A GitHub matrix leg can also run `parallel_rspec -n 2`. When `RAIDER_SHARD_INDEX` and `TEST_ENV_NUMBER` are both present and `PARALLEL_TEST_GROUPS` is a positive integer `g`, the two are composed so every process gets a unique shard:
|
|
174
|
+
|
|
175
|
+
- `index = (RAIDER_SHARD_INDEX - 1) * g + parallel_index`
|
|
176
|
+
- `total = RAIDER_SHARD_TOTAL * g`
|
|
177
|
+
- `label = "<job>#<RAIDER_SHARD_INDEX>.<parallel_index>"`
|
|
178
|
+
|
|
179
|
+
For example, matrix leg 2 of 2 with `parallel_rspec -n 2` yields shards 3 and 4 of 4, labelled `#2.1` and `#2.2`. `RAIDER_SHARD_TOTAL` must match the matrix size. Without it, `total` is null. If `PARALLEL_TEST_GROUPS` is missing or invalid, only `RAIDER_SHARD_INDEX` is used. Every matrix leg must use the same parallel_tests process count (`-n`) and set `RAIDER_SHARD_TOTAL`; mixing different `-n` values, or composed and plain legs, can repeat shard numbers.
|
|
180
|
+
|
|
181
|
+
Tip: run `rm -rf raider-results` before a parallel run. Each process only cleans its own files, so files from an earlier run with more processes stay behind.
|
|
182
|
+
|
|
95
183
|
## Examples
|
|
96
184
|
|
|
97
185
|
- `examples/rspec_basic` is a runnable RSpec project with a passing, a failing (with an attachment) and a skipped example. Run it with `cd examples/rspec_basic && bundle install && bundle exec rspec`; the failure is deliberate.
|
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
|
|
@@ -16,17 +20,73 @@ module RaiderReporter
|
|
|
16
20
|
build_url = if @env["GITHUB_SERVER_URL"] && @env["GITHUB_REPOSITORY"] && run_id
|
|
17
21
|
"#{@env['GITHUB_SERVER_URL']}/#{@env['GITHUB_REPOSITORY']}/actions/runs/#{run_id}"
|
|
18
22
|
end
|
|
19
|
-
|
|
23
|
+
attempt = @env["GITHUB_RUN_ATTEMPT"].to_s.empty? ? "1" : @env["GITHUB_RUN_ATTEMPT"]
|
|
24
|
+
repo = @env["GITHUB_REPOSITORY"]
|
|
25
|
+
build_key = ("github_actions:#{repo}:#{run_id}:#{attempt}" if repo && run_id)
|
|
26
|
+
{ "provider" => "github_actions", "build_url" => build_url, "build_id" => run_id,
|
|
27
|
+
"attempt" => attempt, "job" => @env["GITHUB_JOB"], "build_key" => build_key,
|
|
28
|
+
"repository" => (repo unless repo.to_s.empty?) }.compact
|
|
20
29
|
end
|
|
21
30
|
|
|
22
31
|
def git
|
|
23
|
-
|
|
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 }
|
|
24
35
|
info = info.compact
|
|
25
36
|
info.empty? ? nil : info
|
|
26
37
|
end
|
|
27
38
|
|
|
28
39
|
private
|
|
29
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
|
+
|
|
30
90
|
def branch
|
|
31
91
|
head_ref = @env["GITHUB_HEAD_REF"].to_s
|
|
32
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
|
|
@@ -61,10 +61,10 @@ module RaiderReporter
|
|
|
61
61
|
result
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
def finish_run(ci:, git:, environment:)
|
|
64
|
+
def finish_run(ci:, git:, environment:, shard: nil)
|
|
65
65
|
Run.new(
|
|
66
66
|
client_run_id: @uuid.call, started_at: @started_at, finished_at: @clock.call,
|
|
67
|
-
ci: ci, git: git, environment: environment,
|
|
67
|
+
ci: ci, git: git, environment: environment, shard: shard,
|
|
68
68
|
results: @results.map { |r| r.with(attachments: r.attachments.select { |a| keep?(a) }) }
|
|
69
69
|
)
|
|
70
70
|
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
|
|
@@ -10,5 +10,5 @@ module RaiderReporter
|
|
|
10
10
|
:identifier, :full_name, :file_path, :line_number, :status,
|
|
11
11
|
:duration_ms, :attempts, :tags, :failure, :attachments
|
|
12
12
|
)
|
|
13
|
-
Run = Data.define(:client_run_id, :started_at, :finished_at, :ci, :git, :environment, :results)
|
|
13
|
+
Run = Data.define(:client_run_id, :started_at, :finished_at, :ci, :git, :environment, :results, :shard)
|
|
14
14
|
end
|
|
@@ -28,12 +28,18 @@ module RaiderReporter
|
|
|
28
28
|
}
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
+
def shard_index = @run.shard&.fetch("index")
|
|
32
|
+
|
|
33
|
+
def attachments_dir = shard_index ? "attachments-#{shard_index}" : "attachments"
|
|
34
|
+
|
|
35
|
+
def run_file = shard_index ? "run-#{shard_index}.json" : "run.json"
|
|
36
|
+
|
|
31
37
|
def attachment_entries
|
|
32
38
|
@attachment_entries ||= @run.results.flat_map(&:attachments).each_with_index.map do |attachment, index|
|
|
33
39
|
{
|
|
34
40
|
attachment: attachment,
|
|
35
41
|
part: "att_#{index}",
|
|
36
|
-
path: "
|
|
42
|
+
path: "#{attachments_dir}/#{index}-#{File.basename(attachment.source_path)}"
|
|
37
43
|
}
|
|
38
44
|
end
|
|
39
45
|
end
|
|
@@ -46,7 +52,8 @@ module RaiderReporter
|
|
|
46
52
|
"finished_at" => @run.finished_at&.utc&.iso8601(3),
|
|
47
53
|
"ci" => @run.ci,
|
|
48
54
|
"git" => @run.git,
|
|
49
|
-
"environment" => @run.environment
|
|
55
|
+
"environment" => @run.environment,
|
|
56
|
+
"shard" => @run.shard
|
|
50
57
|
}.compact
|
|
51
58
|
end
|
|
52
59
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RaiderReporter
|
|
4
|
+
# Identifies which slice of a parallel suite this process is.
|
|
5
|
+
class ShardDetector
|
|
6
|
+
def initialize(env: ENV, job: nil)
|
|
7
|
+
@env = env
|
|
8
|
+
@job = job.to_s.strip.empty? ? nil : job.to_s
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def shard
|
|
12
|
+
index, total, suffix = composed || explicit || parallel_tests
|
|
13
|
+
return nil unless index
|
|
14
|
+
|
|
15
|
+
{ "index" => index, "total" => total, "label" => "#{@job || "local"}##{suffix || index}" }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
# A matrix leg that also runs parallel_tests: every process gets a unique index.
|
|
21
|
+
def composed
|
|
22
|
+
explicit_index, explicit_total = explicit
|
|
23
|
+
parallel_index, = parallel_tests
|
|
24
|
+
groups = positive(@env["PARALLEL_TEST_GROUPS"])
|
|
25
|
+
return nil unless explicit_index && parallel_index && groups
|
|
26
|
+
|
|
27
|
+
[(explicit_index - 1) * groups + parallel_index,
|
|
28
|
+
explicit_total && explicit_total * groups,
|
|
29
|
+
"#{explicit_index}.#{parallel_index}"]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def explicit
|
|
33
|
+
index = positive(@env["RAIDER_SHARD_INDEX"])
|
|
34
|
+
[index, positive(@env["RAIDER_SHARD_TOTAL"])] if index
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def parallel_tests
|
|
38
|
+
return nil unless @env.key?("TEST_ENV_NUMBER")
|
|
39
|
+
|
|
40
|
+
raw = @env["TEST_ENV_NUMBER"].to_s
|
|
41
|
+
index = raw.empty? ? 1 : positive(raw)
|
|
42
|
+
[index, positive(@env["PARALLEL_TEST_GROUPS"])] if index
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def positive(value)
|
|
46
|
+
Integer(value.to_s, exception: false)&.then { |n| n.positive? ? n : nil }
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -12,12 +12,12 @@ module RaiderReporter
|
|
|
12
12
|
|
|
13
13
|
def write(payload)
|
|
14
14
|
dir = File.expand_path(@config.output_dir)
|
|
15
|
-
clean(dir) if @config.clean_output_dir
|
|
16
|
-
FileUtils.mkdir_p(File.join(dir,
|
|
15
|
+
clean(dir, payload) if @config.clean_output_dir
|
|
16
|
+
FileUtils.mkdir_p(File.join(dir, payload.attachments_dir))
|
|
17
17
|
payload.attachment_entries.each do |entry|
|
|
18
18
|
FileUtils.cp(entry[:attachment].source_path, File.join(dir, entry[:path]))
|
|
19
19
|
end
|
|
20
|
-
run_json = File.join(dir,
|
|
20
|
+
run_json = File.join(dir, payload.run_file)
|
|
21
21
|
File.write(run_json, JSON.pretty_generate(payload.to_h(mode: :local)))
|
|
22
22
|
run_json
|
|
23
23
|
end
|
|
@@ -25,9 +25,9 @@ module RaiderReporter
|
|
|
25
25
|
private
|
|
26
26
|
|
|
27
27
|
# Remove only what this writer creates, so output_dir "." or "spec" is safe.
|
|
28
|
-
def clean(dir)
|
|
29
|
-
FileUtils.rm_f(File.join(dir,
|
|
30
|
-
FileUtils.rm_rf(File.join(dir,
|
|
28
|
+
def clean(dir, payload)
|
|
29
|
+
FileUtils.rm_f(File.join(dir, payload.run_file))
|
|
30
|
+
FileUtils.rm_rf(File.join(dir, payload.attachments_dir))
|
|
31
31
|
end
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -8,7 +8,7 @@ require "securerandom"
|
|
|
8
8
|
module RaiderReporter
|
|
9
9
|
module Writers
|
|
10
10
|
class Uploader
|
|
11
|
-
Result = Data.define(:ok, :url, :run_id, :error)
|
|
11
|
+
Result = Data.define(:ok, :url, :run_id, :build_url, :error)
|
|
12
12
|
# Keeps uploads under the server's 50 MB request limit and multipart part limit.
|
|
13
13
|
MAX_UPLOAD_ATTACHMENT_BYTES = 45 * 1024 * 1024
|
|
14
14
|
MAX_UPLOAD_ATTACHMENTS = 1000
|
|
@@ -42,7 +42,7 @@ module RaiderReporter
|
|
|
42
42
|
end
|
|
43
43
|
to_result(response)
|
|
44
44
|
rescue StandardError => e
|
|
45
|
-
Result.new(ok: false, url: nil, run_id: nil, error: "#{e.class}: #{e.message}")
|
|
45
|
+
Result.new(ok: false, url: nil, run_id: nil, build_url: nil, error: "#{e.class}: #{e.message}")
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
private
|
|
@@ -93,9 +93,11 @@ module RaiderReporter
|
|
|
93
93
|
def to_result(response)
|
|
94
94
|
if response.is_a?(Net::HTTPSuccess)
|
|
95
95
|
body = parse_body(response.body)
|
|
96
|
-
|
|
96
|
+
build = body["build"].is_a?(Hash) ? body["build"] : {}
|
|
97
|
+
Result.new(ok: true, url: present(body["url"]), run_id: present(body["run_id"]),
|
|
98
|
+
build_url: present(build["url"]), error: nil)
|
|
97
99
|
else
|
|
98
|
-
Result.new(ok: false, url: nil, run_id: nil, error: "HTTP #{response.code}: #{response.body.to_s[0, 200]}")
|
|
100
|
+
Result.new(ok: false, url: nil, run_id: nil, build_url: nil, error: "HTTP #{response.code}: #{response.body.to_s[0, 200]}")
|
|
99
101
|
end
|
|
100
102
|
end
|
|
101
103
|
|
data/lib/raider_reporter.rb
CHANGED
|
@@ -10,7 +10,10 @@ require_relative "raider_reporter/content_types"
|
|
|
10
10
|
require_relative "raider_reporter/collector"
|
|
11
11
|
require_relative "raider_reporter/writers/local"
|
|
12
12
|
require_relative "raider_reporter/ci_detector"
|
|
13
|
+
require_relative "raider_reporter/shard_detector"
|
|
13
14
|
require_relative "raider_reporter/writers/uploader"
|
|
15
|
+
require_relative "raider_reporter/comment/summary_client"
|
|
16
|
+
require_relative "raider_reporter/comment/github_client"
|
|
14
17
|
|
|
15
18
|
module RaiderReporter
|
|
16
19
|
class << self
|
|
@@ -65,11 +68,13 @@ module RaiderReporter
|
|
|
65
68
|
$stderr.puts("[raider_reporter] #{message}")
|
|
66
69
|
end
|
|
67
70
|
|
|
68
|
-
def finish!(output = $
|
|
71
|
+
def finish!(output = $stderr)
|
|
69
72
|
return nil if dry_run?
|
|
70
73
|
|
|
71
74
|
detector = CiDetector.new
|
|
72
|
-
|
|
75
|
+
ci = detector.ci
|
|
76
|
+
shard = ShardDetector.new(job: ci&.fetch("job", nil)).shard
|
|
77
|
+
run = collector.finish_run(ci: ci, git: detector.git, environment: default_environment, shard: shard)
|
|
73
78
|
payload = Payload.new(run)
|
|
74
79
|
begin
|
|
75
80
|
Writers::Local.new(configuration).write(payload)
|
|
@@ -92,7 +97,8 @@ module RaiderReporter
|
|
|
92
97
|
private
|
|
93
98
|
|
|
94
99
|
def success_line(result)
|
|
95
|
-
|
|
100
|
+
link = result.build_url || result.url
|
|
101
|
+
return "Raider report: #{link}" if link
|
|
96
102
|
return "Raider report uploaded (run #{result.run_id})" if result.run_id
|
|
97
103
|
|
|
98
104
|
"Raider report uploaded"
|
metadata
CHANGED
|
@@ -1,34 +1,41 @@
|
|
|
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
|
|
31
37
|
- lib/raider_reporter/rspec/formatter.rb
|
|
38
|
+
- lib/raider_reporter/shard_detector.rb
|
|
32
39
|
- lib/raider_reporter/version.rb
|
|
33
40
|
- lib/raider_reporter/writers/local.rb
|
|
34
41
|
- lib/raider_reporter/writers/uploader.rb
|
|
@@ -53,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
53
60
|
- !ruby/object:Gem::Version
|
|
54
61
|
version: '0'
|
|
55
62
|
requirements: []
|
|
56
|
-
rubygems_version:
|
|
63
|
+
rubygems_version: 3.6.2
|
|
57
64
|
specification_version: 4
|
|
58
65
|
summary: 'Test reporting for Ruby Raider: local run.json and Raider uploads'
|
|
59
66
|
test_files: []
|