test(cli): cover Homebrew upgrade behavior end to end
Some checks are pending
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Waiting to run
TypeScript / Test (push) Waiting to run
TypeScript / Build (push) Waiting to run

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.
This commit is contained in:
Bryan Helmkamp 2026-04-17 18:28:54 -04:00
parent 1530ba4e93
commit e891cb3961
No known key found for this signature in database
3 changed files with 109 additions and 7 deletions

View file

@ -15,7 +15,7 @@ fabro fork <run_id> @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

View file

@ -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 <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 <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`

View file

@ -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`.
");
}