Commit Graph

2 Commits

Author SHA1 Message Date
Jordan Ritter 914dbc5855 fix(showcase/bin): surface GHCR /token exchange failures instead of masking them
bearer_for ended with a blanket `rescue StandardError => nil` plus
`return nil if status >= 400`. Now that bearer_for ALWAYS performs the
/token exchange (even when a token is present), that swallow masked real
failures: a non-2xx /token response, a malformed JSON body, or a transport
error all collapsed to nil, after which manifest_exists issued the manifest
HEAD anonymously (no Authorization). That silently violated manifest_exists's
documented raise-on-transport/5xx contract and conflated "no token supplied"
with "supplied token failed to exchange".

bearer_for now:
- raises GHCR::Error on a >=400 /token response WHEN a token was supplied
  (token absent still returns nil — the legitimate anonymous-public fallback);
- raises GHCR::Error on JSON::ParserError for an unparseable 200 body;
- no longer swallows StandardError, so transport exceptions
  (Errno::ECONNREFUSED, Net::ReadTimeout, ...) propagate.

Call-site enumeration (bearer_for is called only by manifest_exists and
resolve_digest):
- manifest_exists: documents "Raises GHCR::Error on 5xx or transport failure",
  so a raised exchange error is consistent with — and strengthens — its own
  contract. Assumption holds.
- resolve_digest: already raises GHCR::Error on its own manifest HEAD >=400 and
  does not rescue bearer_for, so a raised exchange error propagates exactly as
  its other failures do. Assumption holds.
Neither caller is broken by bearer_for raising; both already propagate
GHCR::Error to their callers.

Tests: add 3 red-green cases in test_ghcr_bearer.rb (token-present 401 raises;
token-present malformed body raises; token-present exchange failure issues NO
anonymous manifest HEAD). The existing manifest_exists/digest fakes were only
green because the old swallow masked the fake's "no fake response" error — they
never modeled the mandatory /token exchange; added a successful /token fake to
each so they exercise the real path. Renumbered the snapshot-ivar-lint
allowlist (+13 lines) to track the bin/railway line drift.
2026-06-04 09:18:08 -07:00
Jordan Ritter 56e85dfd80 feat(showcase): add bin/railway Ruby tooling for Railway ops
Single-file Ruby (stdlib only) with 9 subcommands for Showcase
Railway operations: snapshot, restore, rollback, rollback-commit,
promote, pin, env-diff, resolve-digest, lint-prod.

- Production protection: --yes + typed 'production' confirmation
  (--non-interactive skips the prompt but still requires --yes).
- Uniform exit codes: 0 clean, 1 drift/findings, 2 error.
- GraphQL via backboard.railway.app with serviceInstanceDeployV2.
- GHCR digest resolution via Docker-Content-Digest header.
- Promote prechecks: service-set parity, critical env-key parity,
  custom-domain audit; REFUSE on parity miss, WARN on domain drift.
- Minitest suite (stdlib) covers CLI parsing, snapshot YAML
  roundtrip, GHCR digest decision tree, and production prompt.
- CI workflow showcase_lint_prod.yml runs lint-prod + tests on every
  PR that touches showcase/.
2026-05-28 10:23:24 -07:00