specguard-ruby 0.3.30 → 0.3.31
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/README.md +19 -5
- data/lib/specguard/client/configuration.rb +15 -1
- data/lib/specguard/client/ingest_cli.rb +1 -1
- data/lib/specguard/client/transport.rb +53 -5
- data/lib/specguard/minitest/reporter.rb +12 -2
- data/lib/specguard/rspec/formatter.rb +13 -2
- data/lib/specguard/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 14818808e8ef3b696b1b3834ea590734fa398f5fabbef1c5646d03699e844d99
|
|
4
|
+
data.tar.gz: ccce2e2cf09f6b3401284c0f7f3f343185c86fa2d48bc43ca7bbd9e48575910e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a7612a5d841ecf6d7b1f675bfe362a5141948c1e227a89d7d90e449c9f3b802b650894e7cd3862bb37a897dcbc77ae6e0d627caa1b3cbde3275b54ebd41beb54
|
|
7
|
+
data.tar.gz: d78f0bea385d4a9ca63d1581648350c76662f7540b0c105dda6a0ebb15c7b3be1be98e7d2ca8f29b5536fac597a4ed155cb5b96f8ff8575e79bfde7292ff0034
|
data/README.md
CHANGED
|
@@ -474,6 +474,15 @@ export SPECGUARD_API_KEY=… # from your repository's settings
|
|
|
474
474
|
export SPECGUARD_TIMEOUT=10 # optional; seconds, applied to connect and read
|
|
475
475
|
```
|
|
476
476
|
|
|
477
|
+
**Using an `sga_` agent key? Also set `SPECGUARD_REPOSITORY_ID`.** An agent key
|
|
478
|
+
covers a set of repositories, so the key cannot say which one a run belongs to;
|
|
479
|
+
the request has to. With `SPECGUARD_REPOSITORY_ID=42` the run is POSTed to
|
|
480
|
+
`<endpoint>/api/v1/repositories/42/ingest` instead of `<endpoint>/api/v1/ingest`
|
|
481
|
+
(`config.repository_id` in Ruby). Under a repository `sgk_` key leave it unset —
|
|
482
|
+
if you do set it, the server ignores it. The id may contain only letters, digits,
|
|
483
|
+
`-` and `_`; anything else is refused before a request is made. A `404` with an
|
|
484
|
+
id configured means no repository with that id is available to the key.
|
|
485
|
+
|
|
477
486
|
```ruby
|
|
478
487
|
# ...or in Ruby, if you would rather not use the environment
|
|
479
488
|
SpecGuard.configure do |config|
|
|
@@ -515,6 +524,7 @@ offered to the endpoint and not accepted — so the run can be replayed later wi
|
|
|
515
524
|
```
|
|
516
525
|
SpecGuard: could not deliver test telemetry (HTTP 401 — the API key was not
|
|
517
526
|
accepted). Falling back to log/test_results.jsonl; the test run is unaffected.
|
|
527
|
+
Once the delivery is fixed, replay this run with: bundle exec specguard-ingest log/test_results.jsonl
|
|
518
528
|
```
|
|
519
529
|
|
|
520
530
|
**That line carries the endpoint's own words when it has any.** A `400` refusal
|
|
@@ -527,7 +537,8 @@ SpecGuard: could not deliver test telemetry (HTTP 400 — the endpoint rejected
|
|
|
527
537
|
the payload — spec 3 (spec/orders_spec.rb:9): line_number is required and must
|
|
528
538
|
be a positive integer; spec 7 (spec/orders_spec.rb:31): outcome must be one of
|
|
529
539
|
passed, failed, pending). Falling back to log/test_results.jsonl; the test run
|
|
530
|
-
is unaffected.
|
|
540
|
+
is unaffected. Once the delivery is fixed, replay this run with: bundle exec
|
|
541
|
+
specguard-ingest log/test_results.jsonl
|
|
531
542
|
```
|
|
532
543
|
|
|
533
544
|
It stays **one** line whatever comes back. A systemic problem can have the
|
|
@@ -628,8 +639,9 @@ specguard-ingest: lines 1, 2 carried ci_run_id 17442 and each came back with
|
|
|
628
639
|
test_run_id 41f2c9b8 — the endpoint folded them onto one run
|
|
629
640
|
```
|
|
630
641
|
|
|
631
|
-
It reads the same `SPECGUARD_ENDPOINT`, `SPECGUARD_API_KEY`
|
|
632
|
-
`
|
|
642
|
+
It reads the same `SPECGUARD_ENDPOINT`, `SPECGUARD_API_KEY`, `SPECGUARD_REPOSITORY_ID`
|
|
643
|
+
(for `sga_` agent keys — a replay queue written under one is re-delivered to the same
|
|
644
|
+
repository-scoped route) and `SPECGUARD_TIMEOUT` the formatter does, and sends each line through the same
|
|
633
645
|
transport — so a run that was refused for a rotated key appears on the platform
|
|
634
646
|
once the secret is fixed, with the shard folding described in
|
|
635
647
|
[If you shard your suite](#if-you-shard-your-suite) applying exactly as it would
|
|
@@ -1212,9 +1224,11 @@ export SPECGUARD_ENDPOINT=https://specguard.internal.example.com
|
|
|
1212
1224
|
nothing any other formatter wrote, is read or forwarded.
|
|
1213
1225
|
- **No environment.** SpecGuard's own code reads a fixed list of variables and
|
|
1214
1226
|
no others: the ones named in the envelope table above, which fill
|
|
1215
|
-
`commit_sha`, `branch`, `ci_run_id` and `shard_id`; plus
|
|
1227
|
+
`commit_sha`, `branch`, `ci_run_id` and `shard_id`; plus five that configure
|
|
1216
1228
|
the gem itself rather than describing your suite — `SPECGUARD_ENDPOINT` (where
|
|
1217
|
-
to send the run), `
|
|
1229
|
+
to send the run), `SPECGUARD_REPOSITORY_ID` (which repository an `sga_` agent
|
|
1230
|
+
key's run belongs to — sent as a path segment of the request URL, nothing else),
|
|
1231
|
+
`SPECGUARD_OUTPUT_PATH` (where to write the replay queue —
|
|
1218
1232
|
runs the endpoint refused or could not be reached for), `SPECGUARD_LOCAL_OUTPUT_PATH`
|
|
1219
1233
|
(where to write the local development record when there is no key),
|
|
1220
1234
|
`SPECGUARD_TIMEOUT` (how long to wait), and
|
|
@@ -318,6 +318,7 @@ module SpecGuard
|
|
|
318
318
|
LOCAL_OUTPUT_PATH_KEYS = %w[SPECGUARD_LOCAL_OUTPUT_PATH].freeze
|
|
319
319
|
ENDPOINT_KEYS = %w[SPECGUARD_ENDPOINT].freeze
|
|
320
320
|
API_KEY_KEYS = %w[SPECGUARD_API_KEY].freeze
|
|
321
|
+
REPOSITORY_ID_KEYS = %w[SPECGUARD_REPOSITORY_ID].freeze
|
|
321
322
|
TIMEOUT_KEYS = %w[SPECGUARD_TIMEOUT].freeze
|
|
322
323
|
|
|
323
324
|
# The commit the suite ran against. `nil` when nothing said.
|
|
@@ -344,7 +345,9 @@ module SpecGuard
|
|
|
344
345
|
attr_accessor :local_output_path
|
|
345
346
|
# The SpecGuard installation to POST to, scheme and host only:
|
|
346
347
|
# `https://specguard.example.com`. The `/api/v1/ingest` path is the
|
|
347
|
-
# platform's contract, not a setting
|
|
348
|
+
# platform's contract, not a setting — with one exception: when
|
|
349
|
+
# {#repository_id} is set the path is `/api/v1/repositories/<id>/ingest`
|
|
350
|
+
# instead. `nil` when nothing said.
|
|
348
351
|
attr_accessor :endpoint
|
|
349
352
|
# The repository's SpecGuard API key. Present means "deliver over HTTP".
|
|
350
353
|
#
|
|
@@ -354,6 +357,16 @@ module SpecGuard
|
|
|
354
357
|
# nobody can retroactively fix. Send whatever is configured and let a 401
|
|
355
358
|
# be the answer.
|
|
356
359
|
attr_accessor :api_key
|
|
360
|
+
# The repository the run belongs to, sent as a path segment
|
|
361
|
+
# (`/api/v1/repositories/<id>/ingest`). Needed by an `sga_` agent key,
|
|
362
|
+
# which covers a set of repositories and so cannot name the run's
|
|
363
|
+
# repository itself; under an `sgk_` key the platform ignores it. `nil`
|
|
364
|
+
# when unset or blank, which keeps the plain `/api/v1/ingest` path.
|
|
365
|
+
#
|
|
366
|
+
# Deliberately **not** format-checked against the key prefix or a numeric
|
|
367
|
+
# shape (same reasoning as {#api_key}): the platform's 400/403/404 is the
|
|
368
|
+
# answer. {Transport} only refuses ids that are unsafe as a path segment.
|
|
369
|
+
attr_accessor :repository_id
|
|
357
370
|
# Seconds, applied to opening the connection and to reading the response.
|
|
358
371
|
attr_accessor :timeout
|
|
359
372
|
|
|
@@ -374,6 +387,7 @@ module SpecGuard
|
|
|
374
387
|
@local_output_path = first_present(env, LOCAL_OUTPUT_PATH_KEYS) || DEFAULT_LOCAL_OUTPUT_PATH
|
|
375
388
|
@endpoint = first_present(env, ENDPOINT_KEYS)
|
|
376
389
|
@api_key = first_present(env, API_KEY_KEYS)
|
|
390
|
+
@repository_id = first_present(env, REPOSITORY_ID_KEYS)
|
|
377
391
|
@timeout = seconds(first_present(env, TIMEOUT_KEYS)) || DEFAULT_TIMEOUT_SECONDS
|
|
378
392
|
end
|
|
379
393
|
|
|
@@ -740,7 +740,7 @@ module SpecGuard
|
|
|
740
740
|
raise UsageError, "no API key is configured (set SPECGUARD_API_KEY)" if blank?(configuration.api_key)
|
|
741
741
|
|
|
742
742
|
transport = Transport.new(endpoint: configuration.endpoint, api_key: configuration.api_key,
|
|
743
|
-
timeout: configuration.timeout)
|
|
743
|
+
timeout: configuration.timeout, repository_id: configuration.repository_id)
|
|
744
744
|
# Asked once, here, so a malformed `SPECGUARD_ENDPOINT` is one exit 2
|
|
745
745
|
# rather than N identical `:failed` lines. `Transport#deliver` would
|
|
746
746
|
# otherwise swallow the same `ArgumentError` once per line and report a
|
|
@@ -127,6 +127,21 @@ module SpecGuard
|
|
|
127
127
|
# Part of the platform's contract, so it is not configurable — the
|
|
128
128
|
# `endpoint` setting is the installation's address and nothing more.
|
|
129
129
|
PATH = "/api/v1/ingest"
|
|
130
|
+
|
|
131
|
+
# The repository-scoped form of the same endpoint (`post "ingest"` nested
|
|
132
|
+
# under `resources :repositories`). It exists because an `sga_` agent key
|
|
133
|
+
# covers a SET of repositories, so the credential cannot name the run's
|
|
134
|
+
# repository and the request has to, as a path segment. `%s` is only ever
|
|
135
|
+
# filled with an id that passed {REPOSITORY_ID_PATTERN}. Under an `sgk_`
|
|
136
|
+
# key the server ignores the segment, so {PATH} stays the default.
|
|
137
|
+
REPOSITORY_PATH = "/api/v1/repositories/%s/ingest"
|
|
138
|
+
|
|
139
|
+
# What a repository id may look like before it becomes a path segment.
|
|
140
|
+
# Deliberately not "digits": the platform's ids are numeric today and the
|
|
141
|
+
# gem must not bake that in. Dots are excluded outright, which is what
|
|
142
|
+
# refuses `..` (percent-escaping would not neutralise a dot segment), and
|
|
143
|
+
# `/`, `?`, `#` can never survive as path structure.
|
|
144
|
+
REPOSITORY_ID_PATTERN = /\A[0-9A-Za-z_-]+\z/
|
|
130
145
|
CONTENT_TYPE = "application/json"
|
|
131
146
|
USER_AGENT = "specguard-ruby/#{SpecGuard::VERSION}"
|
|
132
147
|
CONTENT_ENCODING = "gzip"
|
|
@@ -167,7 +182,12 @@ module SpecGuard
|
|
|
167
182
|
# same reason turned around: a 202 whose body will not parse is still an
|
|
168
183
|
# acceptance, and relabelling it would tell the operator something untrue
|
|
169
184
|
# about a run the platform has already stored. See {#test_run_id}.
|
|
170
|
-
|
|
185
|
+
#
|
|
186
|
+
# `repository_scoped` is true when the request went to the repository-
|
|
187
|
+
# scoped route. It only changes the 404 sentence: under that route a 404
|
|
188
|
+
# means no repository with that id is available to this key, not a wrong
|
|
189
|
+
# URL. Nil/false (every pre-existing `Result.new`) keeps today's wording.
|
|
190
|
+
Result = Struct.new(:outcome, :code, :error, :reasons, :body, :repository_scoped, keyword_init: true) do
|
|
171
191
|
# The status codes worth spelling out, because each implies a different
|
|
172
192
|
# thing for the reader to *do*. A 401 means "rotate or fix the key"; a
|
|
173
193
|
# 400 means "the payload this gem built was refused", which is a bug
|
|
@@ -193,6 +213,10 @@ module SpecGuard
|
|
|
193
213
|
# tells the reader how widespread it is.
|
|
194
214
|
MAX_RENDERED_REASONS = 3
|
|
195
215
|
|
|
216
|
+
# The 404 sentence when a repository id was sent. See `repository_scoped`.
|
|
217
|
+
REPOSITORY_NOT_FOUND_ADVICE =
|
|
218
|
+
"no repository with that id is available to this API key — check SPECGUARD_REPOSITORY_ID"
|
|
219
|
+
|
|
196
220
|
# Ceiling on the spelled-out reasons, before the `and N more` tail,
|
|
197
221
|
# which is appended afterwards so the count can never be the thing that
|
|
198
222
|
# gets truncated away.
|
|
@@ -224,13 +248,19 @@ module SpecGuard
|
|
|
224
248
|
def reason
|
|
225
249
|
case outcome
|
|
226
250
|
when :success then nil
|
|
227
|
-
when :rejected then [+"HTTP #{code}",
|
|
251
|
+
when :rejected then [+"HTTP #{code}", advice, rendered_reasons].compact.join(" — ")
|
|
228
252
|
else "#{error.class}: #{error.message}"
|
|
229
253
|
end
|
|
230
254
|
end
|
|
231
255
|
|
|
232
256
|
private
|
|
233
257
|
|
|
258
|
+
def advice
|
|
259
|
+
return REPOSITORY_NOT_FOUND_ADVICE if code == 404 && repository_scoped
|
|
260
|
+
|
|
261
|
+
ADVICE[code]
|
|
262
|
+
end
|
|
263
|
+
|
|
234
264
|
# The refusal, bounded and flattened to fit on the one line. `nil` when
|
|
235
265
|
# there is nothing readable to add, which is what keeps the bare
|
|
236
266
|
# `HTTP 400 — the endpoint rejected the payload` intact for every body
|
|
@@ -270,9 +300,15 @@ module SpecGuard
|
|
|
270
300
|
# @param timeout [Numeric, String, nil] seconds. Anything that is not a
|
|
271
301
|
# positive finite number falls back to the default rather than raising:
|
|
272
302
|
# a typo in `SPECGUARD_TIMEOUT` must not be able to fail a suite.
|
|
273
|
-
|
|
303
|
+
# @param repository_id [String, Integer, nil] when present, POST to the
|
|
304
|
+
# repository-scoped route an `sga_` agent key needs. nil/blank keeps the
|
|
305
|
+
# `sgk_` path byte-identical. An id outside {REPOSITORY_ID_PATTERN} makes
|
|
306
|
+
# {#uri} raise `ArgumentError`.
|
|
307
|
+
def initialize(endpoint:, api_key:, timeout: Configuration::DEFAULT_TIMEOUT_SECONDS, repository_id: nil)
|
|
274
308
|
@endpoint = endpoint
|
|
275
309
|
@api_key = api_key
|
|
310
|
+
@repository_id = repository_id.to_s.strip
|
|
311
|
+
@repository_id = nil if @repository_id.empty?
|
|
276
312
|
@timeout = sanitize_timeout(timeout)
|
|
277
313
|
end
|
|
278
314
|
|
|
@@ -301,7 +337,8 @@ module SpecGuard
|
|
|
301
337
|
return Result.new(outcome: :success, code: code, body: success_body(response)) if
|
|
302
338
|
response.is_a?(Net::HTTPSuccess)
|
|
303
339
|
|
|
304
|
-
Result.new(outcome: :rejected, code: code, reasons: refusal_reasons(response)
|
|
340
|
+
Result.new(outcome: :rejected, code: code, reasons: refusal_reasons(response),
|
|
341
|
+
repository_scoped: !@repository_id.nil?)
|
|
305
342
|
rescue ScriptError, StandardError => e
|
|
306
343
|
# Connection refused, DNS failure, TLS failure, open/read timeout, a
|
|
307
344
|
# malformed endpoint — one family, one shape. `ScriptError` is in the
|
|
@@ -427,7 +464,7 @@ module SpecGuard
|
|
|
427
464
|
base = @endpoint.to_s.strip
|
|
428
465
|
raise ArgumentError, "no endpoint is configured (set SPECGUARD_ENDPOINT)" if base.empty?
|
|
429
466
|
|
|
430
|
-
parsed = URI.parse(base.sub(%r{/+\z}, "") +
|
|
467
|
+
parsed = URI.parse(base.sub(%r{/+\z}, "") + ingest_path)
|
|
431
468
|
# `URI::HTTPS < URI::HTTP`, so this admits both and rejects the
|
|
432
469
|
# scheme-less `specguard.example.com` that `URI.parse` happily returns
|
|
433
470
|
# as a `URI::Generic` with a nil host — which `Net::HTTP` would then
|
|
@@ -438,6 +475,17 @@ module SpecGuard
|
|
|
438
475
|
parsed
|
|
439
476
|
end
|
|
440
477
|
|
|
478
|
+
def ingest_path
|
|
479
|
+
return PATH if @repository_id.nil?
|
|
480
|
+
|
|
481
|
+
unless REPOSITORY_ID_PATTERN.match?(@repository_id)
|
|
482
|
+
raise ArgumentError,
|
|
483
|
+
"SPECGUARD_REPOSITORY_ID must contain only letters, digits, '-' and '_', got #{@repository_id.inspect}"
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
format(REPOSITORY_PATH, @repository_id)
|
|
487
|
+
end
|
|
488
|
+
|
|
441
489
|
def sanitize_timeout(value)
|
|
442
490
|
parsed = Float(value, exception: false)
|
|
443
491
|
return Configuration::DEFAULT_TIMEOUT_SECONDS unless parsed&.finite? && parsed.positive?
|
|
@@ -247,7 +247,8 @@ module SpecGuard
|
|
|
247
247
|
SpecGuard::Client::Transport.new(
|
|
248
248
|
endpoint: @configuration.endpoint,
|
|
249
249
|
api_key: @configuration.api_key,
|
|
250
|
-
timeout: @configuration.timeout
|
|
250
|
+
timeout: @configuration.timeout,
|
|
251
|
+
repository_id: @configuration.repository_id
|
|
251
252
|
)
|
|
252
253
|
end
|
|
253
254
|
|
|
@@ -405,12 +406,21 @@ module SpecGuard
|
|
|
405
406
|
# Report, not promise — see `#fall_back` for why that is the whole of
|
|
406
407
|
# this slice, and why the order above it did not move.
|
|
407
408
|
def sink_clause(path, error)
|
|
408
|
-
return
|
|
409
|
+
return replay_clause(path) if error.nil?
|
|
409
410
|
|
|
410
411
|
"The replay queue #{path} could not be written either " \
|
|
411
412
|
"(#{error.class}: #{error.message}), so this run's telemetry was lost."
|
|
412
413
|
end
|
|
413
414
|
|
|
415
|
+
# The success arm (SPGD-1139): names the configured path (the one
|
|
416
|
+
# `#append` just wrote) and the replay command, word-for-word the
|
|
417
|
+
# formatter's clause. The lost-run arm names no command — nothing to
|
|
418
|
+
# replay.
|
|
419
|
+
def replay_clause(path)
|
|
420
|
+
"Falling back to #{path}; the test run is unaffected. " \
|
|
421
|
+
"Once the delivery is fixed, replay this run with: bundle exec specguard-ingest #{path}"
|
|
422
|
+
end
|
|
423
|
+
|
|
414
424
|
# Once per process, like the formatter's `warn_once`: fifty failing
|
|
415
425
|
# deliveries is one problem, not fifty lines of it.
|
|
416
426
|
def warn_once(message)
|
|
@@ -1040,7 +1040,8 @@ module SpecGuard
|
|
|
1040
1040
|
SpecGuard::Client::Transport.new(
|
|
1041
1041
|
endpoint: configuration.endpoint,
|
|
1042
1042
|
api_key: configuration.api_key,
|
|
1043
|
-
timeout: configuration.timeout
|
|
1043
|
+
timeout: configuration.timeout,
|
|
1044
|
+
repository_id: configuration.repository_id
|
|
1044
1045
|
)
|
|
1045
1046
|
end
|
|
1046
1047
|
|
|
@@ -1202,12 +1203,22 @@ module SpecGuard
|
|
|
1202
1203
|
# which of the two things actually happened — see {#fall_back} for why that
|
|
1203
1204
|
# is the whole of this slice, and why the order above it did not move.
|
|
1204
1205
|
def sink_clause(path, error)
|
|
1205
|
-
return
|
|
1206
|
+
return replay_clause(path) if error.nil?
|
|
1206
1207
|
|
|
1207
1208
|
"The replay queue #{path} could not be written either " \
|
|
1208
1209
|
"(#{error.class}: #{error.message}), so this run's telemetry was lost."
|
|
1209
1210
|
end
|
|
1210
1211
|
|
|
1212
|
+
# The success arm (SPGD-1139): where the run sits AND the command that gets
|
|
1213
|
+
# it back, fix-the-delivery first and replay after — the order the ingest
|
|
1214
|
+
# CLI's own doctrine describes. The lost-run arm above deliberately names no
|
|
1215
|
+
# command: with no queue written there is nothing to replay. Kept
|
|
1216
|
+
# word-for-word identical to the Minitest reporter's clause.
|
|
1217
|
+
def replay_clause(path)
|
|
1218
|
+
"Falling back to #{path}; the test run is unaffected. " \
|
|
1219
|
+
"Once the delivery is fixed, replay this run with: bundle exec specguard-ingest #{path}"
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1211
1222
|
def emit_warning(line)
|
|
1212
1223
|
return if @warned
|
|
1213
1224
|
|
data/lib/specguard/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: specguard-ruby
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.31
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- specguard Agent
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-10-
|
|
11
|
+
date: 2026-10-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: json
|