|
| 1 | +# `bin/railway` |
| 2 | + |
| 3 | +Single-file Ruby tooling for showcase Railway operations. |
| 4 | + |
| 5 | +## Why |
| 6 | + |
| 7 | +The Showcase platform lives on Railway across two environments (staging and |
| 8 | +production). Day-to-day operations — promoting staging to production, pinning |
| 9 | +services to immutable image digests, rolling a bad deploy back, auditing drift |
| 10 | +between envs — used to require ad-hoc shell + GraphQL recipes. `bin/railway` |
| 11 | +makes those operations first-class CLI subcommands with consistent flags, |
| 12 | +exit codes, and production protection. |
| 13 | + |
| 14 | +## Install |
| 15 | + |
| 16 | +None. Requires system Ruby 3.x (stdlib only — no Bundler, no Gemfile). |
| 17 | + |
| 18 | +```sh |
| 19 | +showcase/bin/railway --help |
| 20 | +``` |
| 21 | + |
| 22 | +## Auth |
| 23 | + |
| 24 | +The tool reads a Railway API token from (in order): |
| 25 | + |
| 26 | +1. `RAILWAY_TOKEN` environment variable |
| 27 | +2. `~/.railway/config.json` (the `token` field, or `user.token`) |
| 28 | + |
| 29 | +It never invokes `railway login`, `railway logout`, or `op`. If neither source |
| 30 | +yields a token, it exits with code 2 and a clear error. |
| 31 | + |
| 32 | +For GHCR digest resolution (`resolve-digest`, `pin`), set `GHCR_TOKEN` if you |
| 33 | +need to read private packages; public packages work anonymously via the GHCR |
| 34 | +`/token` endpoint. |
| 35 | + |
| 36 | +## Subcommands |
| 37 | + |
| 38 | +| Subcommand | Purpose | |
| 39 | +|---|---| |
| 40 | +| `snapshot` | Capture an env's services + config into a YAML snapshot. | |
| 41 | +| `restore` | Restore an env to a snapshot (force-redeploy each service). | |
| 42 | +| `rollback` | Roll a single service back one deploy (or to a specific deployment id with `--to`). | |
| 43 | +| `rollback-commit` | Restore an env to the snapshot committed at a given git SHA. | |
| 44 | +| `promote` | Promote staging digests to production with prechecks. | |
| 45 | +| `pin` | Pin a service to a specific image digest. | |
| 46 | +| `env-diff` | Diff two envs; exits 1 on drift. | |
| 47 | +| `resolve-digest` | Resolve an image tag (e.g. `:latest`) to its `sha256:` digest. | |
| 48 | +| `lint-prod` | CI gate: fail if any prod service is not digest-pinned. | |
| 49 | + |
| 50 | +Run any subcommand with `--help` for full flag list. |
| 51 | + |
| 52 | +## Production protection |
| 53 | + |
| 54 | +Every subcommand that mutates state requires both: |
| 55 | + |
| 56 | +- `--yes` flag, **and** |
| 57 | +- typed confirmation of the literal string `production` on stdin |
| 58 | + |
| 59 | +…before any production mutation runs. `--non-interactive` skips the prompt |
| 60 | +but still requires `--yes`. There is no way to mutate production without an |
| 61 | +explicit acknowledgement. |
| 62 | + |
| 63 | +## Exit codes |
| 64 | + |
| 65 | +| Code | Meaning | |
| 66 | +|---|---| |
| 67 | +| 0 | Clean / success | |
| 68 | +| 1 | Drift detected, findings reported, or promote refused for policy reasons | |
| 69 | +| 2 | Error (auth, network, GraphQL schema, refused confirmation, etc.) | |
| 70 | + |
| 71 | +## Worked example: promote staging → production |
| 72 | + |
| 73 | +```sh |
| 74 | +# 1. Audit drift first (read-only). |
| 75 | +showcase/bin/railway env-diff staging production |
| 76 | +# DRIFT: 3 finding(s) |
| 77 | +# service showcase-shell: digest sha256:abc != sha256:def |
| 78 | +# ... |
| 79 | + |
| 80 | +# 2. Lint prod to confirm baseline is pinned. |
| 81 | +showcase/bin/railway lint-prod |
| 82 | +# OK: all production services digest-pinned. |
| 83 | + |
| 84 | +# 3. Capture a "before" snapshot in case we need to roll back. |
| 85 | +showcase/bin/railway snapshot --env production --output before-promote.yaml |
| 86 | + |
| 87 | +# 4. Run the promote with prechecks. Production confirmation prompt fires here. |
| 88 | +showcase/bin/railway promote --yes |
| 89 | +# Type 'production' to confirm promote: production |
| 90 | +# promoted showcase-shell -> ghcr.io/copilotkit/showcase-shell@sha256:def... |
| 91 | +# ... |
| 92 | + |
| 93 | +# If anything goes sideways: |
| 94 | +showcase/bin/railway restore --env production --snapshot before-promote.yaml --yes |
| 95 | +``` |
| 96 | + |
| 97 | +## CI integration |
| 98 | + |
| 99 | +`.github/workflows/showcase_lint_prod.yml` runs `bin/railway lint-prod` on |
| 100 | +every PR that touches `showcase/**`. The job fails (exit 1) if any |
| 101 | +production service has drifted away from a digest pin — that's the contract. |
| 102 | + |
| 103 | +## Tests |
| 104 | + |
| 105 | +```sh |
| 106 | +ruby showcase/bin/spec/all_tests.rb |
| 107 | +``` |
| 108 | + |
| 109 | +Tests are minitest (stdlib). They cover: |
| 110 | + |
| 111 | +- argv parsing per subcommand |
| 112 | +- snapshot YAML round-trip |
| 113 | +- GHCR digest-resolution decision tree (mocked HTTP) |
| 114 | +- production-protection prompt behavior |
| 115 | + |
| 116 | +No Railway / GHCR network calls are made during tests. |
0 commit comments