diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 891d599..4663c16 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,15 @@ name: Release on: + pull_request: + paths: + - ".github/workflows/release.yml" + - "Cargo.toml" + - "Cargo.lock" + - "src/**" + - "tests/**" + - "install.sh" + - "scripts/check-release-prep.sh" push: tags: - "v*" @@ -19,7 +28,7 @@ jobs: - os: ubuntu-latest target: x86_64-unknown-linux-gnu archive: tar.gz - - os: macos-13 + - os: macos-15-intel target: x86_64-apple-darwin archive: tar.gz - os: macos-14 @@ -75,6 +84,7 @@ jobs: name: publish GitHub Release runs-on: ubuntu-latest needs: build + if: startsWith(github.ref, 'refs/tags/') steps: - uses: actions/download-artifact@v4 with: diff --git a/src/version.rs b/src/version.rs index 647daa0..a79ae61 100644 --- a/src/version.rs +++ b/src/version.rs @@ -1,5 +1,5 @@ use crate::config; -use crate::paths::{path_contains_dir, OcvmPaths}; +use crate::paths::{executable_path, path_contains_dir, OcvmPaths}; use crate::project; use crate::source::SourceProvider; use anyhow::{anyhow, Context, Result}; @@ -154,10 +154,12 @@ pub fn install(paths: &OcvmPaths, source: &dyn SourceProvider, requested: &str) let result = (|| { source.install(&version, &staging)?; source.verify_staged_install(&version, &staging)?; - let output = Command::new(staging.join("node_modules").join(".bin").join("openclaw")) - .arg("--version") - .output() - .context("failed to run openclaw --version after install")?; + let output = Command::new(executable_path( + staging.join("node_modules").join(".bin").join("openclaw"), + )) + .arg("--version") + .output() + .context("failed to run openclaw --version after install")?; if !output.status.success() { return Err(anyhow!( "openclaw --version failed: {}", @@ -285,6 +287,7 @@ where let cmd = iter .next() .ok_or_else(|| anyhow!("exec requires a command"))?; + let cmd = resolve_command_in_active_bin(paths, &resolved.version, cmd.as_ref()); let path = std::env::var_os("PATH").unwrap_or_default(); let path = std::env::join_paths( std::iter::once(paths.bin_dir(&resolved.version)).chain(std::env::split_paths(&path)), @@ -302,6 +305,21 @@ where Ok(status.code().unwrap_or(1)) } +fn resolve_command_in_active_bin( + paths: &OcvmPaths, + version: &str, + command: &OsStr, +) -> std::ffi::OsString { + let command_path = Path::new(command); + if command_path.components().count() == 1 { + let active_bin_command = executable_path(paths.bin_dir(version).join(command_path)); + if active_bin_command.exists() { + return active_bin_command.into_os_string(); + } + } + command.to_os_string() +} + pub fn doctor( paths: &OcvmPaths, cwd: &Path, diff --git a/tests/cli.rs b/tests/cli.rs index 4a94015..230c336 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,6 +1,6 @@ use assert_cmd::Command; use predicates::prelude::*; -use std::fs; +use std::{fs, path::PathBuf}; use tempfile::TempDir; fn cmd(home: &TempDir) -> Command { @@ -10,7 +10,23 @@ fn cmd(home: &TempDir) -> Command { command } -fn install_fake(home: &TempDir, version: &str, body: &str) { +fn fake_openclaw_path(bin: PathBuf) -> PathBuf { + if cfg!(windows) { + bin.join("openclaw.cmd") + } else { + bin.join("openclaw") + } +} + +fn fake_openclaw_body(output: &str) -> String { + if cfg!(windows) { + format!("@echo off\r\necho {output}\r\n") + } else { + format!("#!/bin/sh\necho {output}\n") + } +} + +fn install_fake(home: &TempDir, version: &str, output: &str) { let bin = home .path() .join("home") @@ -19,8 +35,8 @@ fn install_fake(home: &TempDir, version: &str, body: &str) { .join("node_modules") .join(".bin"); fs::create_dir_all(&bin).unwrap(); - let openclaw = bin.join("openclaw"); - fs::write(&openclaw, body).unwrap(); + let openclaw = fake_openclaw_path(bin); + fs::write(&openclaw, fake_openclaw_body(output)).unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -44,8 +60,8 @@ fn help_is_available_without_initializing_home() { #[test] fn default_current_list_use_and_uninstall_work_on_local_versions() { let home = TempDir::new().unwrap(); - install_fake(&home, "2026.3.28", "#!/bin/sh\necho 2026.3.28\n"); - install_fake(&home, "2026.4.01", "#!/bin/sh\necho 2026.4.01\n"); + install_fake(&home, "2026.3.28", "2026.3.28"); + install_fake(&home, "2026.4.01", "2026.4.01"); cmd(&home).args(["default", "2026.3.28"]).assert().success(); cmd(&home) @@ -86,8 +102,8 @@ fn default_current_list_use_and_uninstall_work_on_local_versions() { #[test] fn project_pin_wins_and_exec_explicit_does_not_change_default() { let home = TempDir::new().unwrap(); - install_fake(&home, "2026.3.28", "#!/bin/sh\necho default\n"); - install_fake(&home, "2026.4.01", "#!/bin/sh\necho explicit\n"); + install_fake(&home, "2026.3.28", "default"); + install_fake(&home, "2026.4.01", "explicit"); cmd(&home).args(["default", "2026.3.28"]).assert().success(); let project = home.path().join("project"); @@ -127,7 +143,7 @@ fn damaged_default_does_not_block_explicit_healthy_version() { .join(".bin"), ) .unwrap(); - install_fake(&home, "2026.4.01", "#!/bin/sh\necho healthy\n"); + install_fake(&home, "2026.4.01", "healthy"); cmd(&home).args(["default", "2026.3.28"]).assert().success(); cmd(&home) .args(["exec", "2026.4.01", "--", "openclaw"]) @@ -159,8 +175,8 @@ fn init_outputs_shell_helpers() { #[test] fn snapshot_and_rollback_work_from_cli() { let home = TempDir::new().unwrap(); - install_fake(&home, "2026.3.28", "#!/bin/sh\necho old\n"); - install_fake(&home, "2026.4.01", "#!/bin/sh\necho new\n"); + install_fake(&home, "2026.3.28", "old"); + install_fake(&home, "2026.4.01", "new"); cmd(&home).args(["default", "2026.3.28"]).assert().success(); cmd(&home).args(["use", "2026.3.28"]).assert().success(); diff --git a/tests/core.rs b/tests/core.rs index 091176a..5421281 100644 --- a/tests/core.rs +++ b/tests/core.rs @@ -1,12 +1,12 @@ use anyhow::Result; use ocvm::config; -use ocvm::paths::OcvmPaths; +use ocvm::paths::{executable_path, OcvmPaths}; use ocvm::project; use ocvm::source::{RemoteVersion, SourceProvider}; use ocvm::version; use std::cell::RefCell; use std::fs; -use std::path::Path; +use std::path::{Path, PathBuf}; use tempfile::TempDir; struct FakeSource { @@ -46,6 +46,22 @@ impl FakeSource { } } +fn fake_openclaw_path(bin: PathBuf) -> PathBuf { + if cfg!(windows) { + bin.join("openclaw.cmd") + } else { + bin.join("openclaw") + } +} + +fn fake_openclaw_body(output: &str) -> String { + if cfg!(windows) { + format!("@echo off\r\necho {output}\r\n") + } else { + format!("#!/bin/sh\necho {output}\n") + } +} + impl SourceProvider for FakeSource { fn resolve_alias(&self, requested: &str) -> Result { Ok(self @@ -85,8 +101,8 @@ impl SourceProvider for FakeSource { self.installed_specs.borrow_mut().push(version.to_string()); let bin = staging_dir.join("node_modules").join(".bin"); fs::create_dir_all(&bin)?; - let openclaw = bin.join("openclaw"); - fs::write(&openclaw, format!("#!/bin/sh\necho {version}\n"))?; + let openclaw = fake_openclaw_path(bin); + fs::write(&openclaw, fake_openclaw_body(version))?; #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -119,7 +135,7 @@ fn install_stages_verifies_and_creates_shim() { assert_eq!(installed, "2026.3.28"); assert!(paths.openclaw_bin("2026.3.28").exists()); - assert!(paths.shims.join("openclaw").exists()); + assert!(executable_path(paths.shims.join("openclaw")).exists()); assert_eq!(source.installed_specs.borrow().as_slice(), ["2026.3.28"]); }