aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-03 14:38:08 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-03 14:38:08 +0300
commite584f93f367650a2b752a4e3a889603fb81b08f4 (patch)
tree975fdfce6352d53f52a4fd68ffebdb5146beb915
parent22c99852298b7c5a91657f278d5b1c213b6fd6cc (diff)
fix: reconcile update channel switches
-rw-r--r--docs/next/README.md4
-rw-r--r--docs/next/website/src/content/docs/install.mdx6
-rw-r--r--src/cli.rs77
-rw-r--r--src/update.rs56
4 files changed, 128 insertions, 15 deletions
diff --git a/docs/next/README.md b/docs/next/README.md
index d6264f63..57435f8c 100644
--- a/docs/next/README.md
+++ b/docs/next/README.md
@@ -73,16 +73,16 @@ Herdr uses the stable update channel by default. To test preview builds from `ma
```bash
herdr channel set preview
-herdr update
```
To return to stable:
```bash
herdr channel set stable
-herdr update
```
+For direct installs, changing channels also checks that channel and installs its latest binary. If that update fails, run `herdr update` to retry from the configured channel.
+
Preview is only for direct installs managed by Herdr's updater. Homebrew and Nix stay on stable and update through their package managers.
## how it compares
diff --git a/docs/next/website/src/content/docs/install.mdx b/docs/next/website/src/content/docs/install.mdx
index ce39b8d2..52b3e056 100644
--- a/docs/next/website/src/content/docs/install.mdx
+++ b/docs/next/website/src/content/docs/install.mdx
@@ -43,20 +43,20 @@ herdr update
`herdr update` is for installs managed by Herdr's own installer. Homebrew and Nix installs are updated through those package managers instead.
-Herdr uses the stable update channel by default. To opt into preview builds from `master`, set the channel first, then update:
+Herdr uses the stable update channel by default. To opt into preview builds from `master`, set the channel:
```bash
herdr channel set preview
-herdr update
```
Switch back to stable the same way:
```bash
herdr channel set stable
-herdr update
```
+For direct installs, changing channels also checks that channel and installs its latest binary. If that update fails, run `herdr update` to retry from the configured channel.
+
Preview builds are GitHub prereleases published from the current development branch. They are useful when you want fixes before the next stable release, but they can regress. Homebrew and Nix installs do not use the preview channel.
By default, `herdr update` installs the new binary and leaves compatible running sessions alone. If an update changes Herdr's client/server protocol, Herdr asks whether to stop the old server after installing. Stop the old server to use the new version. Stopping exits pane processes. For the default session, run `herdr server stop`, then run `herdr` again. For a named session, run `herdr session stop <name>`, then run `herdr session attach <name>` again.
diff --git a/src/cli.rs b/src/cli.rs
index b6e2fc94..f756b96f 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -77,6 +77,14 @@ fn channel_set(args: &[String]) -> std::io::Result<i32> {
return Ok(2);
};
+ if let Some(reason) = channel_set_preview_rejection(
+ channel,
+ crate::update::preview_channel_rejection_for_current_install(),
+ ) {
+ eprintln!("{reason}.");
+ return Ok(1);
+ }
+
let path = crate::config::config_path();
let content = if path.exists() {
std::fs::read_to_string(&path)?
@@ -112,11 +120,23 @@ fn channel_set(args: &[String]) -> std::io::Result<i32> {
"Herdr update channel set to {channel} in {}.",
path.display()
);
- if channel == "preview" {
- println!("Run `herdr update` to install the latest preview build when ready.");
- } else {
- println!("Run `herdr update` to install the latest stable release when ready.");
+
+ match channel_set_install_action(
+ crate::update::package_manager_channel_update_guidance_for_current_install(),
+ ) {
+ ChannelSetInstallAction::PrintGuidance(guidance) => {
+ println!("{guidance}");
+ return Ok(0);
+ }
+ ChannelSetInstallAction::RunSelfUpdate => {}
}
+
+ if let Err(err) = crate::update::self_update(crate::update::SelfUpdateOptions::default()) {
+ eprintln!("update failed: {err}");
+ eprintln!("Run `herdr update` to retry.");
+ return Ok(1);
+ }
+
Ok(0)
}
@@ -129,6 +149,30 @@ fn parse_channel_set_arg(args: &[String]) -> Option<&str> {
}
}
+fn channel_set_preview_rejection(
+ channel: &str,
+ install_rejection: Option<&'static str>,
+) -> Option<&'static str> {
+ (channel == "preview")
+ .then_some(install_rejection)
+ .flatten()
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum ChannelSetInstallAction {
+ RunSelfUpdate,
+ PrintGuidance(&'static str),
+}
+
+fn channel_set_install_action(
+ package_manager_guidance: Option<&'static str>,
+) -> ChannelSetInstallAction {
+ match package_manager_guidance {
+ Some(guidance) => ChannelSetInstallAction::PrintGuidance(guidance),
+ None => ChannelSetInstallAction::RunSelfUpdate,
+ }
+}
+
fn print_channel_help() {
eprintln!("herdr channel commands:");
eprintln!(" herdr channel show print the configured update channel");
@@ -848,4 +892,29 @@ mod tests {
None
);
}
+
+ #[test]
+ fn channel_set_rejects_package_managed_preview_before_config_write() {
+ assert_eq!(
+ super::channel_set_preview_rejection("preview", Some("no preview")),
+ Some("no preview")
+ );
+ assert_eq!(
+ super::channel_set_preview_rejection("stable", Some("no preview")),
+ None
+ );
+ assert_eq!(super::channel_set_preview_rejection("preview", None), None);
+ }
+
+ #[test]
+ fn channel_set_skips_self_update_for_package_manager_guidance() {
+ assert_eq!(
+ super::channel_set_install_action(Some("use package manager")),
+ super::ChannelSetInstallAction::PrintGuidance("use package manager")
+ );
+ assert_eq!(
+ super::channel_set_install_action(None),
+ super::ChannelSetInstallAction::RunSelfUpdate
+ );
+ }
}
diff --git a/src/update.rs b/src/update.rs
index 0d73b5b1..6d1ad49b 100644
--- a/src/update.rs
+++ b/src/update.rs
@@ -1662,6 +1662,37 @@ fn is_nix_managed_install() -> bool {
is_nix_store_exe_path_following_links(&current_exe)
}
+pub(crate) fn preview_channel_rejection_for_current_install() -> Option<&'static str> {
+ let Ok(current_exe) = env::current_exe() else {
+ return None;
+ };
+
+ preview_channel_rejection_for_exe_path(&current_exe)
+}
+
+pub(crate) fn package_manager_channel_update_guidance_for_current_install() -> Option<&'static str>
+{
+ if is_homebrew_managed_install() {
+ Some("Use `brew update && brew upgrade herdr` to update Homebrew installs.")
+ } else if is_nix_managed_install() {
+ Some("Update through Nix to update Nix-managed Herdr installs.")
+ } else {
+ None
+ }
+}
+
+fn preview_channel_rejection_for_exe_path(path: &Path) -> Option<&'static str> {
+ if is_homebrew_managed_exe_path_following_links(path) {
+ Some(
+ "preview channel is only available for direct Herdr installs; Homebrew installs update through `brew update && brew upgrade herdr`",
+ )
+ } else if is_nix_store_exe_path_following_links(path) {
+ Some("preview channel is only available for direct Herdr installs; Nix installs update through Nix")
+ } else {
+ None
+ }
+}
+
pub(crate) fn is_package_manager_managed_exe_path(path: &Path) -> bool {
is_homebrew_managed_exe_path_following_links(path)
|| is_nix_store_exe_path_following_links(path)
@@ -2155,6 +2186,19 @@ mod tests {
}
#[test]
+ fn preview_channel_is_rejected_for_package_manager_paths() {
+ let homebrew = Path::new("/opt/homebrew/Cellar/herdr/0.6.6/bin/herdr");
+ let nix = Path::new("/nix/store/abc123-herdr-0.6.6/bin/herdr");
+ let direct = Path::new("/home/user/.local/bin/herdr");
+
+ assert!(preview_channel_rejection_for_exe_path(homebrew)
+ .is_some_and(|message| message.contains("Homebrew")));
+ assert!(preview_channel_rejection_for_exe_path(nix)
+ .is_some_and(|message| message.contains("Nix")));
+ assert!(preview_channel_rejection_for_exe_path(direct).is_none());
+ }
+
+ #[test]
fn non_nix_store_path_is_not_detected() {
let path = Path::new("/usr/local/bin/herdr");
@@ -2984,16 +3028,16 @@ mod tests {
#[test]
fn stable_channel_installs_stable_asset_when_current_binary_is_preview() {
- let older_stable = Version::parse("0.6.5").unwrap();
- let preview_base = Version::parse("0.6.6").unwrap();
+ let latest_stable = Version::parse("0.6.6").unwrap();
+ let installed_base = Version::parse("0.6.6").unwrap();
assert!(stable_channel_should_install(
- &older_stable,
- &preview_base,
+ &latest_stable,
+ &installed_base,
true
));
assert!(!stable_channel_should_install(
- &older_stable,
- &preview_base,
+ &latest_stable,
+ &installed_base,
false
));
}