Railway CLI
Find a file
Mahmoud Abdelwahab 44e09c26c0
feat: add auto-update mechanism for CLI (#825)
* feat: add auto-update mechanism for CLI

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent auto-update from blocking CLI exit and fix permission/lock issues

- Don't await download_and_stage on exit; spawn it as fire-and-forget so
  commands like --help return instantly even when an update is available
- Add writability check before attempting self-update download, skipping
  the download entirely for root-owned paths like /usr/local/bin
- Replace acquire-then-drop file lock with PID file guard for package
  manager updates to prevent duplicate concurrent spawns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: cap update task with timeout and add staleness to PID guard

- Move download_and_stage back into the awaited task so the tokio
  runtime doesn't cancel it on exit, but cap handle_update_task with
  a 5-second timeout so short commands like --help aren't blocked
- Write "PID TIMESTAMP" to the lock file and treat entries older than
  10 minutes as stale, fixing the permanent block on Windows where
  is_pid_alive always returned true

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: cache version check timestamp, separate lock files, and clean up control flow

- Write last_update_check even when CLI is up-to-date, preventing a
  GitHub API call on every invocation
- Use separate file paths for self-update flock (update.lock) and
  package-manager PID guard (package-update.pid)
- Return Ok(None) instead of bail! for "already checked today" since
  it's normal control flow, not an error
- Make Preferences::write atomic via temp file + rename

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: improve robustness, Windows support, and rollback UX for auto-update

- Bypass same-day gate for known-pending versions so timed-out downloads
  retry on the next invocation
- Skip spawning update task when running `autoupdate` subcommand to avoid
  racing with preference changes
- Use nix crate for Unix PID liveness check; add Windows implementation
  via winapi instead of conservative fallback
- Detach child update process from console Ctrl+C on Windows
- Add Windows CREATE_NEW_PROCESS_GROUP flag to package manager spawning
- Use scopeguard for write-probe cleanup in install method detection
- Warn when checksums.txt is missing rather than silently skipping
- Improve rollback: back up current binary first, support multi-candidate
  selection via inquire prompt
- Remove freebsd target triple (no release asset published)
- Use nanosecond-precision tmp file names to reduce collision risk

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Updates

* fix: prevent autoupdate disable from applying staged update, handle non-writable shell installs, and preserve retry signal

- Skip try_apply_staged() for both `upgrade` and `autoupdate` subcommands
  so `railway autoupdate disable` doesn't swap the binary before disabling
- Add can_write_binary() check in `railway upgrade` for shell installs in
  non-writable locations (e.g. /usr/local/bin with sudo), guiding users to
  use sudo or reinstall instead of failing with a permission error
- Move clear_latest() from eager call in main to post-success in the
  background task, so a timed-out download preserves the retry signal for
  the next invocation instead of losing it behind the same-day gate

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: clear stale version cache for notification-only installs, lock download_and_stage, and add FreeBSD target

- For installs that can't self-update or auto-run a package manager
  (Homebrew, Cargo, Unknown, non-writable Shell), clear latest_version
  after the notification so the next day's check_update() can discover
  newer releases instead of freezing on the first cached version
- Wrap download_and_stage() with an exclusive file lock (update.lock)
  using double-checked locking to prevent concurrent CLI processes from
  racing on the staged-update directory
- Add FreeBSD x86_64 to detect_target_triple() to match install.sh
  support, preventing shell-installed FreeBSD users from hitting an
  "Unsupported platform" error on upgrade

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only clear version cache when update actually starts, Windows-safe rename, custom bin dir detection, and rollback target tracking

- download_and_stage returns Result<bool> so callers distinguish "lock
  held" (no work done) from a real staging; failed package-manager
  spawns no longer fall through to the notification-only branch that
  clears the cache
- Add rename_replacing() helper that removes the destination on Windows
  before renaming, fixing silent write failures for preferences.json
  and update.json after the first successful write
- Shell install detection now falls back to any parent directory named
  "bin", covering custom --bin-dir installs (~/bin, /opt/bin, etc.)
- Backup filenames include the target triple; rollback filters
  candidates by current architecture to prevent cross-arch restoration

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: Windows version cache write, rollback guards, CI status reporting, preference persistence, and FreeBSD self-update

- Use rename_replacing() in UpdateCheck::write() so Windows cache clears work
- Add interact_or! and can_write_binary() guards to rollback path
- Report CI-disabled state in autoupdate status
- Create ~/.railway dir in Preferences::write() for clean HOME directories
- Remove FreeBSD from self-update targets until release pipeline publishes assets

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: tighten install detection, ensure ~/.railway exists, gate FreeBSD self-update, and preserve notifications when auto-updates disabled

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve update retry signals, propagate preference write errors, and use atomic Windows rename

- Notification-only installs (Homebrew, Cargo, Unknown) now preserve
  latest_version in the cache so the "new version available" notice
  actually shows on the next invocation instead of being cleared before
  the user sees it.
- Detached package-manager updates no longer clear the retry signal on
  spawn — the cached version persists until the user is actually on the
  new version.
- Failed staged-update applies preserve the staged payload for retry
  instead of deleting it; the 7-day staleness TTL handles permanent
  failures.
- `railway upgrade` now clears the version cache after a successful
  update so the next invocation doesn't redundantly re-download.
- Preferences::write() returns Result so `railway autoupdate disable`
  and `railway telemetry disable` report failures instead of silently
  succeeding.
- Standardize timestamp_nanos_opt() on unwrap_or_default() everywhere.
- Windows rename_replacing uses MoveFileExW(MOVEFILE_REPLACE_EXISTING)
  for a single-syscall atomic replace instead of remove+rename.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: deduplicate update-check persistence with persist_latest helper

Consolidate repeated UpdateCheck write blocks in spawn_update_task into
a single persist_latest() method, and skip redundant writes when
check_update() already persisted the timestamp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: sort backups by version instead of mtime to fix Windows CI

list_backups relied on filesystem modification times, which required
write access to set in tests and is fragile across platforms. Sort by
the semver version embedded in the backup filename instead.

Also removes low-value tests that only assert trivial string formatting
or compiler-guaranteed match exhaustiveness.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: detach shell downloads, expire stale versions, reduce exit delay, and clarify update message

- Spawn background downloads in a detached child process so they survive
  beyond the parent's exit timeout instead of restarting from zero
- Add download_failures counter to version.json; after 3 consecutive
  failures, clear the cached version to force a fresh API re-check
  (fixes infinite retry loop on yanked/stale releases)
- Reduce exit timeout from 5s to 2s since it now only gates the fast
  API version check, not the download
- Append "(active on next run)" to the auto-update message since the
  current process still executes the old binary

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: extract env var constant, name failure threshold, skip redundant spawns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: stale comment, probe cleanup, non-TTY gating, Windows .old.exe, rollback prune timing, and check_update timeout

- Correct "5 s" → "2 s" in download_and_stage comment to match handle_update_task
- Simplify can_write_binary probe to unconditional create-then-remove
- Skip spawn_update_task in non-TTY when auto-update is disabled
- Clean up leftover .old.exe on Windows at the start of try_apply_staged
- Defer backup pruning until after rollback succeeds so candidates aren't removed before the picker
- Add 30 s timeout to check_update reqwest client to prevent indefinite hangs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: send update notifications to stderr to avoid corrupting command output

println! in the auto-update and version-banner paths wrote to stdout,
which breaks JSON parsing when the CLI is invoked programmatically
(e.g. by Claude Code piping `railway status --json`).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: clear stale update cache when CLI catches up to cached version

Once the installed binary reaches or surpasses the cached latest_version,
clear the cache so spawn_update_task falls through to a fresh check_update().
This prevents repeated package-manager spawns on every invocation after an
update lands and allows discovery of newer releases on manual install paths.

Also null out stdin on the detached package-manager update process to match
spawn_background_download and fully detach from the terminal.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: always attempt fresh API check so cached version does not become permanent

Previously spawn_update_task skipped check_update() entirely when a
cached version existed, and re-persisted the timestamp on every
invocation. This prevented the same-day gate from ever expiring, so the
CLI would advertise a stale cached version forever and never discover
newer releases.

Now we always call check_update() first (gated to once per UTC day),
falling back to the cached version only within the same day for download
retries.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: apply cargo fmt and lint-fix

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: keep cached pending version until staged update is actually applied

background_stage_update() was clearing the cached latest_version
immediately after staging succeeded, but the binary isn't replaced until
try_apply_staged() runs on a later invocation. If that apply step fails,
the user loses both the upgrade banner and future download retries until
the 7-day stale TTL expires. The cache is already cleared on successful
apply in try_apply_staged(), so the staging path should leave it alone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: apply cargo fmt

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: hold update lock across apply in interactive upgrade and rollback

self_update_interactive() dropped the update lock after staging but
before applying, letting a concurrent try_apply_staged() race the binary
replacement. Now the lock is held across both staging and apply.

rollback() was mutating the binary and cleaning staged state without
acquiring update.lock at all, which could interleave with a background
auto-updater. Now it acquires the same lock before replacing the binary.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: correct autoupdate status for non-writable shell installs and close stage-apply race

update_strategy() now checks can_self_update() and can_write_binary()
so `autoupdate status` no longer claims "Background download + auto-swap"
when the binary directory is not writable or the platform is unsupported.

self_update_interactive() now holds a single lock across both
download_and_stage_inner() and apply_staged_update(), eliminating the
window where a concurrent try_apply_staged() could consume the staged
binary between staging and applying.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: drop lock file handle instead of deleting to prevent concurrent lock race

Removing the lock file while the handle is still held unlinks the inode
on Unix, allowing a concurrent process to create a new file at the same
path and acquire its own "exclusive" lock. Replacing remove_file with
drop releases the lock via the OS and leaves the file as an inert
sentinel.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: extract spawn_detached helper and simplify update task logic

- Extract shared spawn_detached() into util/mod.rs, deduplicating the
  detached-process setup from spawn_background_download and
  spawn_package_manager_update
- Remove redundant staged-update check in spawn_background_download
  (child process already checks in download_and_stage)
- Consolidate scattered from_cache/persist_latest branches into a
  single needs_persist flag
- Cache is_auto_update_disabled() result in main() instead of calling
  it twice
- Use persist_latest(None) in check_update's up-to-date branch
- Trim narrating comments

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: skip rolled-back version in auto-update instead of pausing entirely

After rollback, record the rolled-back-from version as skipped in
version.json. Auto-update skips only that version and resumes normally
once a newer release is published. This avoids silent staleness from
a full pause while still respecting the user's rollback intent.

- Add skipped_version field to UpdateCheck (serde-default for compat)
- Record skip in rollback(), guard try_apply_staged() against the race
  where a pre-rollback detached download stages the skipped version
- Clear skip on successful update (clear_after_update) and autoupdate enable
- Suppress "new version available" notification for skipped version
- Show skipped version in autoupdate status

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: close concurrency races in rollback, package-manager guard, and failure reset

- Acquire update.lock before backup/picker in rollback() so a concurrent
  try_apply_staged() cannot swap the binary during interactive selection
- Serialize the PID-check-spawn-write sequence in spawn_package_manager_update()
  with a file lock to prevent two CLI invocations from both launching updaters
- Clear last_update_check alongside latest_version after 3 download failures
  so the same-day gate does not suppress the fresh API re-check

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add checksum generation, respect disable preference, and lock staged cleanup

- Add generate-checksums job to release workflow that produces checksums.txt
  with SHA-256 hashes for all release assets, making checksum verification
  functional instead of always falling back to the skip path
- Gate update polling and "New version available" banner on auto_update_enabled
  so disabled users no longer hit the GitHub API or see nagging banners
- Acquire update.lock in autoupdate disable before cleaning staged updates so
  an in-flight background download cannot re-stage after cleanup returns

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* revert: remove checksums.txt workflow change (pre-existing, out of scope)

The missing checksums.txt in the release pipeline predates this PR.
Reverting the workflow change to keep this PR focused on auto-update fixes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Revert "revert: remove checksums.txt workflow change (pre-existing, out of scope)"

This reverts commit e1e76a1ea2.

* fix: harden auto-update safety for non-TTY, install detection, and permissions

- Gate background updates and staged-apply on is_tty so cron jobs,
  scripts, and piped invocations never silently self-mutate
- Resolve symlinks in InstallMethod::detect() to prevent Intel Homebrew
  misclassification when current_exe() returns the symlink path
- Replace catch-all bin/ Shell detection with explicit allowlist of
  known shell-installer directories to avoid overwriting version-manager
  binaries
- Probe binary directory writability in can_auto_run_package_manager()
  to prevent repeated doomed npm/Bun/Scoop updates on sudo installs
- Acquire package-update lock in autoupdate disable to wait for
  in-flight package manager updates before returning

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: wait for detached updater by PID, restore custom bin-dir support, fix status accuracy

- autoupdate disable now polls the actual child PID instead of
  acquiring a lock the detached process never holds, so it truly
  waits for in-flight npm/Bun/Scoop updates (30s timeout with warning)
- Replace install-method allowlist with version-manager exclusion list
  (asdf, mise, rtx, proto, volta, fnm, nodenv, rbenv, pyenv) and
  restore the */bin/ catch-all so custom --bin-dir installs from
  install.sh are correctly classified as Shell again
- update_strategy() now reflects the writability check for
  npm/Bun/Scoop, showing "Notification only" when the binary
  directory is not writable instead of overpromising

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve rollback guard on enable, recover from missing staged binary, quiesce detached child on disable

- `railway autoupdate enable` no longer clears skipped_version; the
  rollback guard persists until a newer release is applied
- Staged-update fast paths now verify the binary exists before
  short-circuiting; missing binary cleans stale metadata immediately
- Detached background child re-checks auto-update preferences before
  downloading and again after acquiring the update lock

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: exclude check_updates from auto-update side effects

check_updates is a read-only command but was not in the
update-management guard, so it could apply staged binaries and
spawn background downloads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* style: reformat matches! macro (cargo fmt)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: close pkg-manager race on disable, unblock same-day discovery after rollback, fix Windows upgrade instructions

- spawn_package_manager_update re-checks is_auto_update_disabled()
  after acquiring its lock, preventing a concurrent invocation from
  spawning an updater after the user ran autoupdate disable
- skip_version() now clears last_update_check so the next invocation
  performs a fresh API check and can discover a newer release published
  the same day as the rollback
- Upgrade/rollback non-writable instructions now show Windows-appropriate
  guidance (run as Administrator) instead of sudo/bash commands

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: keep checking for releases while skipped version is latest, acquire package-update lock in disable

- check_update() no longer arms the daily gate when the discovered
  version matches skipped_version, so a fix release published later
  the same day is discovered on the next invocation
- autoupdate disable now acquires package_update_lock (blocking) before
  reading the PID file, closing the race where spawn_package_manager_update
  writes the PID after disable already looked

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve package-update PID file when child outlives the 30s timeout

disable was unconditionally removing the PID file even when the
detached updater was still alive. This left no in-flight marker,
so re-enabling auto-updates could launch a duplicate updater.
Now the PID file is only removed once the child has actually exited.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: ensure ~/.railway directory exists before creating rollback lock file

On a fresh install where ~/.railway has never been written,
rollback would fail with "Failed to create update lock file"
instead of reporting no backups available.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: detect custom CARGO_INSTALL_ROOT installs via .crates.toml marker

The */bin catch-all could misclassify a custom Cargo install root
(e.g. CARGO_INSTALL_ROOT=~/tools) as a shell install, allowing the
auto-updater to self-replace a Cargo-managed binary. Now checks for
Cargo's .crates.toml marker in the parent of bin/ before falling
through to Shell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: re-read version state after network call to prevent rollback skip loss, check Cargo marker before shell-path heuristic

check_update() loaded version.json, did a network call (up to 30s), then
wrote the stale snapshot back — silently overwriting a skipped_version set
by a concurrent rollback.  Now re-reads from disk after the network call.

InstallMethod::detect() matched /usr/local/bin before the .crates.toml
marker check, so CARGO_INSTALL_ROOT=/usr/local installs were misclassified
as Shell.  Moved the marker check above the shell-path heuristic.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: don't stamp daily gate when clearing stale version, exclude pnpm from npm detection

clear_latest() called persist_latest(None) which set last_update_check=now,
preventing the background task from discovering a hotfix published later the
same day after a manual upgrade.  Now clears the cached version without
stamping the gate.

pnpm global paths contain "npm" as a substring, causing misclassification
as InstallMethod::Npm and driving updates through the wrong package manager.
Added an early pnpm check that falls back to Unknown.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: serialize interactive upgrade with background package-manager updates

run_upgrade_command() now acquires the same file lock and checks the PID
file used by spawn_package_manager_update(), preventing concurrent
package-manager processes against the same global install when a user
runs `railway upgrade` while a background auto-update is in flight.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: prevent update side effects on help/error paths and retain cached version on API failure

Use raw args to detect update-management subcommands so that clap
DisplayHelp/DisplayVersion error paths (e.g. `railway upgrade --help`)
no longer trigger try_apply_staged() as a side effect.

Match on the Result from check_update() instead of using `?` so that
API errors fall back to the cached known_version for retry rather than
short-circuiting the entire update task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip auto-update pipeline entirely on help/version/parse-error paths

--help, --version, and invalid-input paths now bypass both
try_apply_staged() and the background update spawn, ensuring they are
truly read-only with zero added latency.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: extract parse_pid_file helper and remove dead .crates.toml check

- Extract `parse_pid_file()` in check_update.rs to deduplicate PID file
  parsing across autoupdate, upgrade, and check_update modules
- Remove unreachable `.crates.toml` guard in install_method.rs catch-all
  (the same check already fires earlier in detect())

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: don't stamp daily gate for skipped version, make checksum upload idempotent

- After rollback, an API failure would fall back to the cached (skipped)
  version and call persist_latest(), arming the daily gate. This prevented
  re-checking for a newer release until the next day. Now skipped versions
  skip persist_latest so the API is re-checked on every invocation.
- Add --clobber to gh release upload so re-running the checksums job
  doesn't fail on an existing asset.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip update side effects on bare `railway`, use compile-time target triple

- Bare `railway` (no subcommand) now skips try_apply_staged() and the
  background updater, matching the read-only behavior of --help/--version.
  Previously a first-time user typing `railway` to explore would trigger
  update side effects before seeing help.

- detect_target_triple() now uses the compile-time BUILD_TARGET from
  build.rs instead of runtime OS/arch guessing. This ensures the
  self-updater fetches the correct ABI variant (e.g. a binary built for
  x86_64-unknown-linux-gnu will not be replaced with a musl build).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: treat `railway help` as read-only, fix i686-pc-windows-gnu asset format

- Add "help" to the read-only invocation guard so `railway help` skips
  try_apply_staged() and the background updater, matching the behavior
  of --help and bare `railway`.

- i686-pc-windows-gnu is cross-compiled on Linux and only ships as
  .tar.gz (no .zip). The asset name logic now accounts for this, and
  extraction derives the format from the asset name rather than
  re-checking the target.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: clear daily gate on version catch-up, restore shell upgrade path, eager download spawn

- clear_latest() now resets last_update_check so that same-day hotfixes
  are discovered after the user catches up to a cached version.

- Shell installs on unsupported self-update platforms (e.g. FreeBSD) now
  get a dedicated match arm showing the reinstall command instead of
  falling through to the vague catch-all message.

- spawn_update_task now starts the background download from the cached
  version before the API call, so the 2s exit timeout cannot strand the
  spawn on slow networks. The API check still runs to refresh the cache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: only eagerly spawn download when same-day gate is armed

The eager download spawn could race with a newer version discovered by
the API: the cached-version child holds the update lock, so the
newer-version child exits immediately, and try_apply_staged would apply
the stale cached release on the next run.

Fix: only eagerly spawn when the same-day gate is armed (last_update_check
is today). In that case check_update(false) returns Ok(None) instantly
without a network request, so the API cannot discover a newer version that
would conflict. When the gate is NOT armed, the API call goes over the
network and may return a newer release — defer the spawn to after the
check completes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: ensure parent directory exists before creating detached-process log file

spawn_detached() creates the log file without ensuring the parent
directory exists. On a fresh install where ~/.railway doesn't exist yet,
this fails silently (the spawn result is ignored), preventing the
background download from starting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: show update banner when auto-update disabled, apply staged offline, add PID TTL to disable

- Version discovery and the "New version available" banner now run
  regardless of auto-update preference. Disabling auto-update stops
  automatic installation, not release awareness.

- `railway upgrade` now falls back to an already-staged update when the
  network check fails, so offline users can apply a previously
  downloaded binary.

- `autoupdate disable` now applies the 10-minute PID file TTL before
  trusting the stored PID, consistent with upgrade and
  spawn_package_manager_update. Prevents blocking 30s on a recycled PID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: validate staged update before offline apply in interactive upgrade

The fallback path in self_update_interactive now applies the same guards
as try_apply_staged: rejects stale entries, wrong-platform binaries,
versions <= current, and versions the user rolled back from. Prevents
offline `railway upgrade` from downgrading or re-applying a skipped
version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: simplify auto-update design after review

- Switch version check gate from calendar-day UTC to fixed 12h window
- Remove checksums.txt machinery (code, tests, workflow job) — co-located
  checksums don't add security; TLS handles integrity in transit
- Reduce exit-delay timeout from 2s to 1s for version check
- Simplify `autoupdate disable` — set flag and clean staged, no PID polling
- Run version check in non-TTY to keep cache fresh for script-heavy users
- Suppress update banner when disabled via env var or CI, keep for preference
- Extract `validate_staged()` to deduplicate safety checks
- Enrich `autoupdate status` with pipeline state (latest version, staged,
  last check time, in-flight PID)
- Add `autoupdate skip` subcommand for package-manager installs
- Split download timeout: 30s background, 120s interactive
- Replace count-based package-manager retry (5 attempts) with time gate (1/hr)
- Add telemetry event for silent auto-update apply
- Clean up orphaned generate-checksums workflow job

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: simplify PID liveness check, remove unnecessary duplication

- Extract `is_package_update_running()` helper to replace 3 identical
  PID-file-parse + age-check + liveness-check blocks
- Extract `PID_STALENESS_TTL_SECS` constant (was magic number 600)
- Remove unnecessary clone in `autoupdate skip`
- Remove TOCTOU `.exists()` check before `hard_link` in backup
- Remove unnecessary comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address review findings — TOCTOU, handle leak, error visibility

- Move validate_staged() inside the exclusive lock in try_apply_staged()
  to close TOCTOU window where another process could delete the staged
  binary between validation and apply
- Use std::mem::forget on detached Child handles to avoid leaking OS
  resources on Windows (both package-manager and background-download paths)
- Use rename_replacing() in replace_binary() on Unix for consistency
- Replace silent `let _ = write()` in cache mutation methods with
  try_write() that logs warnings on failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: collapse clippy warning, document intentional handle leaks, defer Windows cleanup

- Collapse nested `if` in spawn_update_task to fix clippy::collapsible_if
- Add comment in download_and_stage_inner explaining the duplicate
  staged-version check is the authoritative post-lock re-check
- Document std::mem::forget(child) on both detached-process call sites
- Move Windows clean_old_binary() to only run after successful apply
  instead of on every try_apply_staged entry

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip API check in disabled non-TTY sessions, validate staged binary, add --locked to cargo install

- Non-TTY sessions with auto-update disabled no longer make a GitHub API
  call or pay the exit-time budget (CI/scripts).
- validate_staged() now checks the staged binary exists on disk before
  reporting the update as ready to apply.
- Cargo upgrade command includes --locked to match documented install path
  and prevent resolver drift.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: simplify auto-update code — deduplicate dispatch, extract helpers, reduce boilerplate

- Extract `write_atomic()` in util/mod.rs, replacing 3 duplicated
  temp-file + rename write implementations
- Extract `try_dispatch_update()` helper and `UpdateContext` struct,
  eliminating duplicated dispatch logic and double InstallMethod::detect()
- Add `UpdateCheck::mutate()` to absorb read-modify-write boilerplate
  from 5 static methods
- Collapse 3 near-identical disabled-state branches in autoupdate status
  into one computed `disabled_reason`
- Trim WHAT-narration comments to concise WHY reasoning

No behavior changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 10:21:50 +09:00
.cargo Add attribute logging (#455) 2024-05-05 11:32:47 +01:00
.github Fix auto-release to checkout master instead of PR ref (#769) 2026-01-19 19:32:51 -05:00
bin release tarfiles for windows (#325) 2023-03-05 17:28:27 +00:00
npm-install fix npm install on windows (#341) 2023-03-08 14:00:56 -05:00
src feat: add auto-update mechanism for CLI (#825) 2026-04-06 10:21:50 +09:00
.dockerignore feat: alpine dockerfile (#470) 2023-12-05 16:52:45 -05:00
.gitattributes Add --latest flag to logs cmd and activeDeployments to status (#719) 2026-01-07 01:32:42 -05:00
.gitignore Add dev command for local development with Docker Compose (#710) 2025-12-12 18:18:58 -05:00
build.rs feat: add auto-update mechanism for CLI (#825) 2026-04-06 10:21:50 +09:00
Cargo.lock feat: add auto-update mechanism for CLI (#825) 2026-04-06 10:21:50 +09:00
Cargo.toml feat: add auto-update mechanism for CLI (#825) 2026-04-06 10:21:50 +09:00
CLAUDE.md Add CLAUDE.md with development guidance (#661) 2025-09-09 20:20:10 -04:00
CONTRIBUTING.md Point to railway.com (#585) 2025-01-08 14:33:37 -05:00
Dockerfile fix exec permission not set in railway-cli docker image (#531) 2024-08-14 13:46:58 -04:00
flake.lock Migrate to CLI v3 (#304) 2023-03-03 21:44:32 -05:00
flake.nix Fix CLI compilation (#514) 2024-06-24 16:41:23 -03:00
install.sh Show telemetry notice on install instead of first run (#832) 2026-04-03 18:45:07 +09:00
LICENSE Migrate to CLI v3 (#304) 2023-03-03 21:44:32 -05:00
package.json chore: Release railwayapp version 4.36.1 2026-04-03 09:51:06 +00:00
pnpm-lock.yaml fix npm install 2023-03-04 09:25:40 -05:00
README.md update readme (#767) 2026-01-16 10:21:39 -05:00
release.toml fix release.toml (again) 2023-04-13 11:28:17 -04:00
shell.nix fix: update nix shell pins to provide Rust 1.94 for edition2024 support (#812) 2026-03-13 15:07:44 -04:00
v2.sh add v2 install script 2023-03-04 17:16:15 -05:00

Railway CLI

Crates.io CI cargo audit

Overview

This is the command line interface for Railway. Use it to connect your code to Railway's infrastructure without needing to worry about environment variables or configuration.

The Railway command line interface (CLI) connects your code to your Railway project from the command line.

The Railway CLI allows you to:

  • Create new Railway projects from the terminal
  • Link to an existing Railway project
  • Pull down environment variables for your project locally to run
  • Create services and databases right from the comfort of your fingertips

And more.

Documentation

View the CLI guide

View the CLI API reference

Quick start

Follow the CLI guide to install the CLI and run your first command.

Authentication

For non-interactive authentication details, see the CLI guide.

Installation

Package managers

Cargo

cargo install railwayapp --locked

Homebrew

brew install railway

NPM

npm install -g @railway/cli

Bash

# Install
bash <(curl -fsSL cli.new)

# Uninstall
bash <(curl -fsSL cli.new) -r

Scoop

scoop install railway

Arch Linux AUR

Install with Paru

paru -S railwayapp-cli

Install with Yay

yay -S railwayapp-cli

Docker

Install from the command line

docker pull ghcr.io/railwayapp/cli:latest

Use in GitHub Actions

For GitHub Actions setup, see the blog post at blog.railway.com/p/github-actions.

Use in GitLab CI/CD

For GitLab CI/CD setup, see the blog post at blog.railway.com/p/gitlab-ci-cd.

Contributing

See CONTRIBUTING.md for information on setting up this repository locally.

Feedback

We would love to hear your feedback or suggestions. The best way to reach us is on Central Station.

We also welcome pull requests into this repository. See CONTRIBUTING.md for information on setting up this repository locally.