Module: SpecGuard::RSpec::ValidatorBackend::Installer

Defined in:
lib/specguard/rspec/validator_backend.rb

Overview

Obtains a validate-intent binary for the DEFAULT-ON path: the platform-matched prebuilt asset from open-test-intent's GitHub release, verified against the release's SHA256SUMS manifest, cached under the user's cache dir so the network is touched once per machine.

Why the download is verified the way Runner#verify! verifies

The posture is inherited from the schema-contract check: a downloaded blob is not a binary because we asked for one. The release publishes a SHA256SUMS manifest beside its assets, and the gem checks the ONE row that names this platform's asset — the same choice open-test-intent's own scripts/install.sh makes, for the reason its header gives (sha256sum -c would report the three rows this host did not stage as missing files). Only a row that matches BOTH the digest and the asset name counts, so a manifest that does not describe the fetched bytes is a refusal and not a pass.

Why a failed fetch is an exit 2 naming two remediations

There is no Ruby fallback any more, so "no binary" must be loud: the message names the env var (point at a binary you already have) and the install.sh curl|sh path (the CI-pinned alternative). A first run with no network is the expected shape of this failure and the message is written for it.

Constant Summary collapse

RELEASE_TAG =

The release the gem resolves against. Pinned by tag, not "latest": which validator validated a CI job must not change because somebody published a new release, and Runner#verify_schema_contract! pins the fetched binary to this gem's vendored schema anyway. v0.1.4 is the first release whose JSON findings carry intent on passing rows; v0.1.3 validated the same schema but reported no payload, which the formatter's mapping reads as "unannotated" — every annotation silently dropped while the verdicts stayed green (found via yatfa-ai/specguard's CI reporting 0.0% annotated after its lock jumped 0.2.3 -> 0.3.1 and the SPGD-867 binary cutover ran for real).

"v0.1.4"
REPOSITORY =
"yatfa-ai/open-test-intent"
DOWNLOAD_BASE =
"https://github.com/#{REPOSITORY}/releases/download/#{RELEASE_TAG}"
INSTALL_SH =

The CI-pinned alternative to the auto-install: install the same verified release artifact onto PATH and point the env var at it.

"curl -fsSL " \
"https://raw.githubusercontent.com/#{REPOSITORY}/#{RELEASE_TAG}/scripts/install.sh | sh"
REMEDIATIONS =

Both remediations, spelled once and reused by every refusal, so the message cannot drift between the ways the fetch can fail.

"set #{ValidatorBackend::ENV_VAR} to a validate-intent binary, " \
"or install one with: #{INSTALL_SH}"
CACHE_DIR_VAR =

Where the cached binary lives when the user has not redirected it. Honours SPECGUARD_CACHE_DIR (hermetic CI caches), then XDG, then ~/.cache for hosts without XDG set.

"SPECGUARD_CACHE_DIR"
OS_FOR =

RUBY_PLATFORM -> the release's os/arch vocabulary. Anything outside the four published assets is unsupported: guessable names (freebsd?) would produce a 404 masquerading as a policy, so the mapping is closed and the refusal names the platform it was asked about.

{ /linux/ => "linux", /darwin|mac/ => "darwin" }.freeze
ARCH_FOR =
{ /x86_64|amd64|x64/ => "amd64", /aarch64|arm64/ => "arm64" }.freeze
OPEN_TIMEOUT =

Net::HTTP timeouts. Short on purpose: this runs at the front of a lint step, and a first-run fetch that hangs is worse than one that fails fast with the remediation message.

10
READ_TIMEOUT =
30
MAX_REDIRECTS =
5

Class Method Summary collapse

Class Method Details

.asset_name(platform: RUBY_PLATFORM) ⇒ Object

The release asset name for this host.

Parameters:

  • platform (String) (defaults to: RUBY_PLATFORM)

    a RUBY_PLATFORM-shaped string; injectable so the mapping is testable off exotic hosts

Raises:



440
441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/specguard/rspec/validator_backend.rb', line 440

def asset_name(platform: RUBY_PLATFORM)
  os = OS_FOR.find { |pattern, _| pattern.match?(platform) }&.last
  arch = ARCH_FOR.find { |pattern, _| pattern.match?(platform) }&.last

  if os.nil? || arch.nil?
    raise ValidatorError,
          "no prebuilt validate-intent release asset for #{platform} " \
          "(#{RELEASE_TAG} publishes #{OS_FOR.values.uniq.product(ARCH_FOR.values.uniq) \
            .map { |o, a| "#{o}/#{a}" }.join(', ')}) — #{REMEDIATIONS}"
  end

  "validate-intent-#{os}-#{arch}"
end

.cache_dir(env) ⇒ String

Parameters:

  • env (Hash, ENV)

Returns:

  • (String)


456
457
458
459
460
461
462
463
464
# File 'lib/specguard/rspec/validator_backend.rb', line 456

def cache_dir(env)
  override = env[CACHE_DIR_VAR].to_s.strip
  xdg = env["XDG_CACHE_HOME"].to_s.strip
  root = override.empty? ? (xdg.empty? ? File.join(Dir.home, ".cache") : xdg) : override
  # Named for the gem, so a cached validator binary survives gem upgrades
  # within one name and is re-downloaded once — and only once — when the
  # gem itself changes name.
  File.join(root, "specguard-ruby", "validate-intent", RELEASE_TAG)
end

.install(asset, destination) ⇒ Object

Downloads the asset plus its manifest, verifies, and installs atomically (temp file + rename, chmod 0755) so a killed download can never leave a half-written binary the next run mistakes for good — or worse, executes.



470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
# File 'lib/specguard/rspec/validator_backend.rb', line 470

def install(asset, destination)
  manifest = download("#{DOWNLOAD_BASE}/SHA256SUMS")
  expected = manifest_digest(manifest, asset)

  bytes = download("#{DOWNLOAD_BASE}/#{asset}")
  actual = Digest::SHA256.hexdigest(bytes)
  unless actual == expected
    raise ValidatorError,
          "downloaded #{DOWNLOAD_BASE}/#{asset} has sha256:#{actual}, but the release " \
          "manifest says sha256:#{expected} — the download is not the artifact the " \
          "release published, so it was not installed; #{REMEDIATIONS}"
  end

  dir = File.dirname(destination)
  FileUtils.mkdir_p(dir)
  tmp = "#{destination}.tmp.#{Process.pid}"
  File.binwrite(tmp, bytes)
  File.chmod(0o755, tmp)
  File.rename(tmp, destination)
  destination
rescue ValidatorError
  raise
rescue StandardError => e
  # Net::HTTP's failures (SocketError, OpenTimeout, EOFError, ...),
  # and any filesystem failure writing the cache. All of them mean
  # "no binary could be obtained", which is the one refusal this
  # module has — exit 2, both remediations.
  raise ValidatorError, "could not obtain validate-intent from #{DOWNLOAD_BASE}: " \
                        "#{e.class}: #{e.message}; #{REMEDIATIONS}"
end

.obtain(env: ENV) ⇒ String

Returns path to an executable, SHA256SUMS-verified binary.

Parameters:

  • env (Hash, ENV) (defaults to: ENV)

Returns:

  • (String)

    path to an executable, SHA256SUMS-verified binary

Raises:

  • (ValidatorError)

    when the platform is unsupported or the binary cannot be obtained or verified



426
427
428
429
430
431
432
433
# File 'lib/specguard/rspec/validator_backend.rb', line 426

def obtain(env: ENV)
  asset = asset_name
  destination = File.join(cache_dir(env), asset)

  return destination if cached?(destination)

  install(asset, destination)
end