From e891cb39618a1240226eb0a2fa28774a4624a3e2 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Fri, 17 Apr 2026 18:28:54 -0400 Subject: [PATCH] test(cli): cover Homebrew upgrade behavior end to end Add process-level upgrade tests that invoke the real fabro binary from a fake Homebrew Cellar path so current_exe() detection is exercised end to end. Update the CLI reference and changelog to document the Homebrew-managed upgrade path and the flags that remain self-managed-only. --- docs/changelog/2026-03-15.mdx | 2 +- docs/reference/cli.mdx | 13 ++- lib/crates/fabro-cli/tests/it/cmd/upgrade.rs | 101 ++++++++++++++++++- 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/docs/changelog/2026-03-15.mdx b/docs/changelog/2026-03-15.mdx index abfb9589d..41d8d825f 100644 --- a/docs/changelog/2026-03-15.mdx +++ b/docs/changelog/2026-03-15.mdx @@ -15,7 +15,7 @@ fabro fork @3 # fork from checkpoint index 3 ## Self-update with fabro upgrade -Fabro can now update itself. `fabro upgrade` downloads the latest release from GitHub, verifies its SHA256 checksum, and atomically replaces the binary. Downgrade protection prevents accidentally installing an older version (override with `--force`). A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in `~/.fabro/cli.toml` or `--no-upgrade-check`. +Fabro can now update itself. For self-managed installs, `fabro upgrade` downloads the latest release from GitHub, verifies its SHA256 checksum, and atomically replaces the binary. Downgrade protection prevents accidentally installing an older version (override with `--force`). On Homebrew installs, `fabro upgrade` leaves the Homebrew-managed binary alone and prints the matching `brew upgrade fabro` or `brew upgrade fabro-nightly` command instead. A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in `~/.fabro/cli.toml` or `--no-upgrade-check`. ```bash fabro upgrade # upgrade to latest diff --git a/docs/reference/cli.mdx b/docs/reference/cli.mdx index 43b7e5bf3..6e8c3aa14 100644 --- a/docs/reference/cli.mdx +++ b/docs/reference/cli.mdx @@ -770,7 +770,9 @@ fabro doctor --server https://fabro.example.com:3000/api/v1 ## `fabro upgrade` -Download and install a newer version of Fabro from GitHub releases. Verifies the download with SHA256 checksums and atomically replaces the binary. +For self-managed installs, download and install a newer version of Fabro from GitHub releases. The CLI verifies the download with SHA256 checksums and atomically replaces the binary. + +If Fabro was installed via Homebrew, `fabro upgrade` does not overwrite the Homebrew-managed binary. It prints the matching `brew upgrade fabro` or `brew upgrade fabro-nightly` command instead. ```bash fabro upgrade @@ -781,13 +783,14 @@ fabro upgrade --version v0.177.0-alpha.1 | Flag | Description | |---|---| -| `--version ` | Target version (e.g. `0.5.0`, `v0.5.0`, or `v0.177.0-alpha.1`). Defaults to the latest stable release. | -| `--force` | Upgrade even if already on the target version | +| `--version ` | Target version (e.g. `0.5.0`, `v0.5.0`, or `v0.177.0-alpha.1`) for self-managed installs. Defaults to the latest stable release. | +| `--prerelease` | For self-managed installs, select the highest semver across stable and prerelease tags. | +| `--force` | For self-managed installs, upgrade even if already on the target version. | | `--dry-run` | Preview what would happen without making changes | -Plain `fabro upgrade` tracks the latest stable release. Use `--version` to install an explicitly named pre-release. +Plain `fabro upgrade` tracks the latest stable release. On self-managed installs, use `--version` to install an explicitly named pre-release or `--prerelease` to follow the newest prerelease automatically. -Fabro refuses to downgrade unless you specify an explicit `--version`. A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in [`settings.toml`](/reference/user-configuration#upgrade_check) or the `--no-upgrade-check` global flag. +Fabro refuses to downgrade unless you specify an explicit `--version`. On Homebrew installs, `--version`, `--prerelease`, and `--force` are not supported because Homebrew selects the version/channel. A daily background check notifies you when a new version is available — disable it with `upgrade_check = false` in [`settings.toml`](/reference/user-configuration#upgrade_check) or the `--no-upgrade-check` global flag. ## `fabro artifact list` diff --git a/lib/crates/fabro-cli/tests/it/cmd/upgrade.rs b/lib/crates/fabro-cli/tests/it/cmd/upgrade.rs index 5b70e9a70..d432332ff 100644 --- a/lib/crates/fabro-cli/tests/it/cmd/upgrade.rs +++ b/lib/crates/fabro-cli/tests/it/cmd/upgrade.rs @@ -1,4 +1,50 @@ -use fabro_test::{fabro_snapshot, test_context}; +use assert_cmd::Command; +use fabro_test::{TestContext, fabro_snapshot, test_context}; + +fn hard_link_or_copy(src: &std::path::Path, dest: &std::path::Path) { + match std::fs::hard_link(src, dest) { + Ok(()) => {} + Err(_) => { + std::fs::copy(src, dest).expect("copy test binary into fake Cellar"); + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + + let perms = std::fs::metadata(src) + .expect("read source binary metadata") + .permissions() + .mode(); + std::fs::set_permissions(dest, std::fs::Permissions::from_mode(perms)) + .expect("preserve executable permissions"); + } + } + } +} + +fn brew_command(context: &TestContext, formula: &str, version: &str) -> Command { + let bin_dir = context.temp_dir.join("Cellar").join(formula).join(version).join("bin"); + std::fs::create_dir_all(&bin_dir).expect("create fake Cellar bin dir"); + let brew_fabro = bin_dir.join("fabro"); + hard_link_or_copy(std::path::Path::new(env!("CARGO_BIN_EXE_fabro")), &brew_fabro); + + let mut cmd = Command::new(&brew_fabro); + cmd.current_dir(&context.temp_dir); + for (key, _) in std::env::vars_os() { + if let Some(s) = key.to_str() { + if s.starts_with("FABRO_") { + cmd.env_remove(&key); + } + } + } + cmd.env("NO_COLOR", "1"); + cmd.env("HOME", &context.home_dir); + cmd.env("FABRO_NO_UPGRADE_CHECK", "true") + .env("FABRO_HTTP_PROXY_POLICY", "disabled") + .env("FABRO_TELEMETRY", "off") + .env("FABRO_SERVER_MAX_CONCURRENT_RUNS", "64") + .env("FABRO_TEST_IN_MEMORY_STORE", "1"); + cmd +} #[test] fn help() { @@ -133,3 +179,56 @@ esac target: [TARGET] "); } + +#[test] +fn upgrade_brew_install_refuses_and_prints_brew_command() { + let context = test_context!(); + let mut cmd = brew_command(&context, "fabro", "0.176.2"); + cmd.args(["upgrade"]); + + fabro_snapshot!(context.filters(), cmd, @" + success: false + exit_code: 1 + ----- stdout ----- + ----- stderr ----- + fabro was installed via Homebrew. + Run `brew upgrade fabro` to update. + error: refusing to overwrite a Homebrew-managed binary + "); +} + +#[test] +fn upgrade_brew_install_dry_run_json_reports_brew_command() { + let context = test_context!(); + let mut cmd = brew_command(&context, "fabro-nightly", "0.205.0-nightly.0"); + cmd.args(["--json", "upgrade", "--dry-run"]); + + fabro_snapshot!(context.filters(), cmd, @r#" + success: true + exit_code: 0 + ----- stdout ----- + { + "install_source": "homebrew", + "formula": "fabro-nightly", + "brew_command": "brew upgrade fabro-nightly", + "dry_run": true, + "note": "fabro is Homebrew-managed; no in-place upgrade attempted" + } + ----- stderr ----- + "#); +} + +#[test] +fn upgrade_brew_install_rejects_version_flag() { + let context = test_context!(); + let mut cmd = brew_command(&context, "fabro", "0.176.2"); + cmd.args(["upgrade", "--version", "0.1.0"]); + + fabro_snapshot!(context.filters(), cmd, @" + success: false + exit_code: 1 + ----- stdout ----- + ----- stderr ----- + error: fabro is managed by Homebrew (formula `fabro`); Homebrew selects the version and channel. Use `brew upgrade fabro` (or reinstall with a different formula) instead of `fabro upgrade --version`/`--prerelease`/`--force`. + "); +}