raider_reporter 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1e069eeb6569206c085d042b6b3e26f0e35bbb2459b60dccfecf6d744aaef86
4
- data.tar.gz: f117594302d75a2d97edfba870beb04e69d477acbb6f80ab276689e55d3c531a
3
+ metadata.gz: 7c1a8baed9855a5cba10100e57b9052184f6f748b7f015d630541d52905031c6
4
+ data.tar.gz: cc40652b897de2d3832a3d5da58d8bdb6f8e3a6873b11da927b68fd5a0478e24
5
5
  SHA512:
6
- metadata.gz: ad5162cad62d6efed699f663f977540ef84f1dc640845d112c8f594bac34767aec0224a341e55e4ebb354b5a59b57bc13c5b4deacd071b8ade6bbade90b85366
7
- data.tar.gz: 6b68c2e3ab889e30a2ff1e9c6cb075f7e3e8556d92557e60f36360daf232e9f9ed72d8041592f8766c8496bc5d303700222e2b96f1df685b8813ece4627c1375
6
+ metadata.gz: e16586146bc9d569b5cd827c5533e6aae6cdef3bec2bc4ae3d6390c230a193ad56be6cc8fd01d11e90e933522a7f2f2ae54613450acb5a95daa609364634af1e
7
+ data.tar.gz: 134a1d1de73ff5b93c00740996ffea7cc8cde10f7be727bbb2ab8320b3158b710ac2df413d3e68ce334bff3fc7b3d40cd5cf2150fcc4cb6cd21d9b0e79521554
data/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0 — 2026-10-11
4
+
5
+ - Report the CI build key, attempt and job so runs of one build are grouped (GitHub Actions)
6
+ - 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
7
+ - Write parallel-safe local output (`run-<n>.json`, `attachments-<n>/`) when sharded; fixes parallel_tests processes overwriting each other's results
8
+ - Print the Raider build URL after upload, falling back to the run URL
9
+
10
+ ## 0.1.1 — 2026-10-11
11
+
12
+ - Send run timestamps with millisecond precision, so durations under a second are no longer reported as 0
13
+
3
14
  ## 0.1.0 — 2026-10-11
4
15
 
5
16
  - RSpec formatter writing Raider schema v1 to `raider-results/run.json`
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 server's response has no URL). If either value is missing, nothing is uploaded and the local files are still written.
87
+ On success the formatter prints `Raider report: <url>`, 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,64 @@ 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
+ ## Parallel runs
96
+
97
+ 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.
98
+
99
+ Shard detection, in order of precedence:
100
+
101
+ 1. `RAIDER_SHARD_INDEX` (1 or more) and, optionally, `RAIDER_SHARD_TOTAL`.
102
+ 2. `parallel_tests` / `parallel_rspec`: `TEST_ENV_NUMBER` (empty means shard 1) and `PARALLEL_TEST_GROUPS` for the total.
103
+ 3. Otherwise the run is not sharded.
104
+
105
+ A GitHub Actions matrix needs only the two variables:
106
+
107
+ ```yaml
108
+ jobs:
109
+ test:
110
+ runs-on: ubuntu-latest
111
+ strategy:
112
+ matrix:
113
+ shard: [1, 2, 3, 4]
114
+ steps:
115
+ - uses: actions/checkout@v4
116
+ - uses: ruby/setup-ruby@v1
117
+ with:
118
+ bundler-cache: true
119
+ - run: bundle exec rspec # split the spec files by shard however you like
120
+ env:
121
+ RAIDER_TOKEN: ${{ secrets.RAIDER_TOKEN }}
122
+ RAIDER_ENDPOINT: ${{ vars.RAIDER_ENDPOINT }}
123
+ RAIDER_SHARD_INDEX: ${{ matrix.shard }}
124
+ RAIDER_SHARD_TOTAL: 4
125
+ ```
126
+
127
+ `parallel_rspec` works with no configuration, because it sets `TEST_ENV_NUMBER` and `PARALLEL_TEST_GROUPS` for each process.
128
+
129
+ 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:
130
+
131
+ ```
132
+ raider-results/
133
+ run-1.json
134
+ attachments-1/
135
+ run-2.json
136
+ attachments-2/
137
+ ```
138
+
139
+ Unsharded runs still write `run.json` and `attachments/`. Tools that read local results should load `run*.json`.
140
+
141
+ ### Matrix and parallel_tests together
142
+
143
+ 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:
144
+
145
+ - `index = (RAIDER_SHARD_INDEX - 1) * g + parallel_index`
146
+ - `total = RAIDER_SHARD_TOTAL * g`
147
+ - `label = "<job>#<RAIDER_SHARD_INDEX>.<parallel_index>"`
148
+
149
+ 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.
150
+
151
+ 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.
152
+
95
153
  ## Examples
96
154
 
97
155
  - `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.
@@ -16,7 +16,11 @@ module RaiderReporter
16
16
  build_url = if @env["GITHUB_SERVER_URL"] && @env["GITHUB_REPOSITORY"] && run_id
17
17
  "#{@env['GITHUB_SERVER_URL']}/#{@env['GITHUB_REPOSITORY']}/actions/runs/#{run_id}"
18
18
  end
19
- { "provider" => "github_actions", "build_url" => build_url, "build_id" => run_id }.compact
19
+ attempt = @env["GITHUB_RUN_ATTEMPT"].to_s.empty? ? "1" : @env["GITHUB_RUN_ATTEMPT"]
20
+ repo = @env["GITHUB_REPOSITORY"]
21
+ build_key = ("github_actions:#{repo}:#{run_id}:#{attempt}" if repo && run_id)
22
+ { "provider" => "github_actions", "build_url" => build_url, "build_id" => run_id,
23
+ "attempt" => attempt, "job" => @env["GITHUB_JOB"], "build_key" => build_key }.compact
20
24
  end
21
25
 
22
26
  def git
@@ -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
@@ -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: "attachments/#{index}-#{File.basename(attachment.source_path)}"
42
+ path: "#{attachments_dir}/#{index}-#{File.basename(attachment.source_path)}"
37
43
  }
38
44
  end
39
45
  end
@@ -42,11 +48,12 @@ module RaiderReporter
42
48
 
43
49
  def run_hash
44
50
  {
45
- "started_at" => @run.started_at&.utc&.iso8601,
46
- "finished_at" => @run.finished_at&.utc&.iso8601,
51
+ "started_at" => @run.started_at&.utc&.iso8601(3),
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RaiderReporter
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  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, "attachments"))
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, "run.json")
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, "run.json"))
30
- FileUtils.rm_rf(File.join(dir, "attachments"))
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
- Result.new(ok: true, url: present(body["url"]), run_id: present(body["run_id"]), error: nil)
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
 
@@ -10,6 +10,7 @@ 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"
14
15
 
15
16
  module RaiderReporter
@@ -69,7 +70,9 @@ module RaiderReporter
69
70
  return nil if dry_run?
70
71
 
71
72
  detector = CiDetector.new
72
- run = collector.finish_run(ci: detector.ci, git: detector.git, environment: default_environment)
73
+ ci = detector.ci
74
+ shard = ShardDetector.new(job: ci&.fetch("job", nil)).shard
75
+ run = collector.finish_run(ci: ci, git: detector.git, environment: default_environment, shard: shard)
73
76
  payload = Payload.new(run)
74
77
  begin
75
78
  Writers::Local.new(configuration).write(payload)
@@ -92,7 +95,8 @@ module RaiderReporter
92
95
  private
93
96
 
94
97
  def success_line(result)
95
- return "Raider report: #{result.url}" if result.url
98
+ link = result.build_url || result.url
99
+ return "Raider report: #{link}" if link
96
100
  return "Raider report uploaded (run #{result.run_id})" if result.run_id
97
101
 
98
102
  "Raider report uploaded"
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.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Augustin Gottlieb
@@ -29,6 +29,7 @@ files:
29
29
  - lib/raider_reporter/payload.rb
30
30
  - lib/raider_reporter/rspec.rb
31
31
  - lib/raider_reporter/rspec/formatter.rb
32
+ - lib/raider_reporter/shard_detector.rb
32
33
  - lib/raider_reporter/version.rb
33
34
  - lib/raider_reporter/writers/local.rb
34
35
  - lib/raider_reporter/writers/uploader.rb