From e2e04f7de60a71834481da718c00c578e48d6348 Mon Sep 17 00:00:00 2001 From: Chris O'Neil Date: Fri, 24 May 2024 23:33:13 +0100 Subject: [PATCH] feat: provide `node-launchpad` command BREAKING CHANGE: the new settings file will have additional entries for keeping track of the node-launchpad installation. Previously serialised settings files will be incompatible with this change. Users will need to clear their previous settings file when they upgrade. --- .github/workflows/pr.yml | 42 ++++++++++++++++++++++++ Cargo.toml | 2 +- src/cmd.rs | 4 +++ src/install.rs | 39 +++++++++++++++++++--- src/main.rs | 43 ++++++++++++++++++++++++ src/update.rs | 71 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 196 insertions(+), 5 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5b775a0..1bde96a 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -13,6 +13,7 @@ env: CLIENT_VERSION: 0.77.27 NODE_VERSION: 0.83.25 NODE_MANAGER_VERSION: 0.1.8 + NODE_LAUNCHPAD_VERSION: 0.2.0 jobs: # The code in this crate uses lots of conditional compilation for cross-platform capabilities. @@ -141,6 +142,7 @@ jobs: cargo run -- client --version $env:CLIENT_VERSION cargo run -- node --version $env:NODE_VERSION cargo run -- node-manager --version $env:NODE_MANAGER_VERSION + cargo run -- node-launchpad --version $env:NODE_LAUNCHPAD_VERSION - name: Check if binaries are available in new shell session shell: pwsh run: | @@ -156,6 +158,10 @@ jobs: Write-Host "safenode-manager.exe does not exist" exit 1 } + if (!(Test-Path "$env:USERPROFILE\safe\node-launchpad.exe")) { + Write-Host "node-launchpad.exe does not exist" + exit 1 + } # For security reasons, the GHA infrastructure prevents environment variables # being modified easily, so we need to refer to the binaries with their full paths. @@ -197,6 +203,18 @@ jobs: exit 1 } + $output = & "${env:USERPROFILE}\safe\node-launchpad.exe" --version + $version = $output | Select-String -Pattern "node-launchpad (\d+\.\d+\.\d+)" + $versionNumber = $version.Matches.Groups[1].Value + if ($versionNumber -eq "$env:NODE_LAUNCHPAD_VERSION") { + Write-Host "The correct version of safenode-manager has been installed" + } else { + Write-Host "The correct version of safenode-manager has not been installed" + Write-Host "We expected version $env:NODE_LAUNCHPAD_VERSION" + Write-Host "The downloaded binary has $versionNumber" + exit 1 + } + test-install-linux: if: "!startsWith(github.event.pull_request.title, 'Automated version bump')" name: Install test (Linux) @@ -214,6 +232,7 @@ jobs: cargo run -- client --version $CLIENT_VERSION cargo run -- node --version $NODE_VERSION cargo run -- node-manager --version $NODE_MANAGER_VERSION + cargo run -- node-launchpad --version $NODE_LAUNCHPAD_VERSION - name: Check if binaries are available in new shell session shell: bash run: | @@ -229,6 +248,7 @@ jobs: [[ -f "$HOME/.local/bin/safe" ]] || { echo "safe not in expected location"; exit 1; } [[ -f "$HOME/.local/bin/safenode" ]] || { echo "safenode not in expected location"; exit 1; } [[ -f "$HOME/.local/bin/safenode-manager" ]] || { echo "safenode-manager not in expected location"; exit 1; } + [[ -f "$HOME/.local/bin/node-launchpad" ]] || { echo "node-launchpad not in expected location"; exit 1; } version=$(safe --version | awk '{ print $2 }') if [[ "$version" == "$CLIENT_VERSION" ]]; then @@ -260,6 +280,16 @@ jobs: exit 1 fi + version=$(node-launchpad --version | awk '{ print $2 }') + if [[ "$version" == "$NODE_LAUNCHPAD_VERSION" ]]; then + echo "The correct version of node-launchpad has been installed" + else + echo "The correct version of node-launchpad has not been installed" + echo "We expected $NODE_LAUNCHPAD_VERSION" + echo "The downloaded binary has $version" + exit 1 + fi + test-install-macos: if: "!startsWith(github.event.pull_request.title, 'Automated version bump')" name: Install test (macOS) @@ -276,6 +306,7 @@ jobs: cargo run -- client --version $CLIENT_VERSION cargo run -- node --version $NODE_VERSION cargo run -- node-manager --version $NODE_MANAGER_VERSION + cargo run -- node-launchpad --version $NODE_LAUNCHPAD_VERSION - name: Check if binaries are available in new shell session run: | # As with the Ubuntu test, we need to source the env file to get the binaries on PATH. @@ -284,6 +315,7 @@ jobs: [[ -f "$HOME/.local/bin/safe" ]] || { echo "safe not in expected location"; exit 1; } [[ -f "$HOME/.local/bin/safenode" ]] || { echo "safenode not in expected location"; exit 1; } [[ -f "$HOME/.local/bin/safenode-manager" ]] || { echo "safenode-manager not in expected location"; exit 1; } + [[ -f "$HOME/.local/bin/node-launchpad" ]] || { echo "node-launchpad not in expected location"; exit 1; } version=$(safe --version | awk '{ print $2 }') if [[ "$version" == "$CLIENT_VERSION" ]]; then @@ -315,6 +347,16 @@ jobs: exit 1 fi + version=$(node-launchpad --version | awk '{ print $2 }') + if [[ "$version" == "$NODE_LAUNCHPAD_VERSION" ]]; then + echo "The correct version of node-launchpad has been installed" + else + echo "The correct version of node-launchpad has not been installed" + echo "We expected $NODE_LAUNCHPAD_VERSION" + echo "The downloaded binary has $version" + exit 1 + fi + test-publish: if: "!startsWith(github.event.pull_request.title, 'Automated version bump')" name: Test Publish diff --git a/Cargo.toml b/Cargo.toml index b8c9d1d..045add5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ semver = "1.0.4" serde = "1.0" serde_derive = "1.0" serde_json = "1.0" -sn-releases = "0.2.0" +sn-releases = "0.2.5" tempfile = "3.8.1" textwrap = "0.16.0" tokio = { version = "1.26", features = ["full"] } diff --git a/src/cmd.rs b/src/cmd.rs index 2d080fc..9abc9b8 100644 --- a/src/cmd.rs +++ b/src/cmd.rs @@ -173,6 +173,10 @@ async fn do_install_binary( settings.safenode_path = Some(bin_path); settings.safenode_version = Some(installed_version); } + AssetType::NodeLaunchpad => { + settings.node_launchpad_path = Some(bin_path); + settings.node_launchpad_version = Some(installed_version); + } AssetType::NodeManager => { settings.safenode_manager_path = Some(bin_path); settings.safenode_manager_version = Some(installed_version); diff --git a/src/install.rs b/src/install.rs index dba97d1..d3943d3 100644 --- a/src/install.rs +++ b/src/install.rs @@ -53,6 +53,7 @@ const SET_PATH_FILE_CONTENT: &str = indoc! {r#" pub enum AssetType { Client, Node, + NodeLaunchpad, NodeManager, } @@ -65,6 +66,7 @@ impl AssetType { match self { AssetType::Client => ReleaseType::Safe, AssetType::Node => ReleaseType::Safenode, + AssetType::NodeLaunchpad => ReleaseType::NodeLaunchpad, AssetType::NodeManager => ReleaseType::SafenodeManager, } } @@ -75,6 +77,7 @@ impl std::fmt::Display for AssetType { match *self { AssetType::Client => write!(f, "safe"), AssetType::Node => write!(f, "safenode"), + AssetType::NodeLaunchpad => write!(f, "node-launchpad"), AssetType::NodeManager => write!(f, "safenode-manager"), } } @@ -82,6 +85,12 @@ impl std::fmt::Display for AssetType { #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Settings { + pub node_launchpad_path: Option, + #[serde( + serialize_with = "serialize_version", + deserialize_with = "deserialize_version" + )] + pub node_launchpad_version: Option, pub safe_path: Option, #[serde( serialize_with = "serialize_version", @@ -166,6 +175,8 @@ impl Settings { let mut contents = String::new(); file.read_to_string(&mut contents)?; serde_json::from_str(&contents).unwrap_or_else(|_| Settings { + node_launchpad_path: None, + node_launchpad_version: None, safe_path: None, safe_version: None, safenode_path: None, @@ -175,6 +186,8 @@ impl Settings { }) } else { Settings { + node_launchpad_path: None, + node_launchpad_version: None, safe_path: None, safe_version: None, safenode_path: None, @@ -206,6 +219,14 @@ impl Settings { } None } + AssetType::NodeLaunchpad => { + if self.node_launchpad_path.is_some() { + let path = self.node_launchpad_path.as_ref().unwrap(); + let version = self.node_launchpad_version.as_ref().unwrap(); + return Some((path.clone(), version.clone())); + } + None + } AssetType::NodeManager => { if self.safenode_manager_path.is_some() { let path = self.safenode_manager_path.as_ref().unwrap(); @@ -422,6 +443,7 @@ fn get_bin_name(asset_type: &AssetType) -> String { let mut bin_name = match asset_type { AssetType::Client => "safe".to_string(), AssetType::Node => "safenode".to_string(), + AssetType::NodeLaunchpad => "node-launchpad".to_string(), AssetType::NodeManager => "safenode-manager".to_string(), }; if OS == "windows" { @@ -788,10 +810,12 @@ mod test { safenode_bin_file.write_binary(b"fake safenode code")?; let safenode_manager_bin_file = tmp_data_path.child("safenode-manager"); safenode_manager_bin_file.write_binary(b"fake safenode-manager code")?; - let testnet_bin_file = tmp_data_path.child("testnet"); - testnet_bin_file.write_binary(b"fake testnet code")?; + let node_launchpad_bin_file = tmp_data_path.child("node-launchpad"); + node_launchpad_bin_file.write_binary(b"fake launchpad code")?; let settings = Settings { + node_launchpad_path: Some(node_launchpad_bin_file.to_path_buf()), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(safe_bin_file.to_path_buf()), safe_version: Some(Version::new(0, 75, 1)), safenode_path: Some(safenode_bin_file.to_path_buf()), @@ -804,6 +828,11 @@ mod test { settings_file.assert(predicates::path::is_file()); let settings = Settings::read(&settings_file.to_path_buf())?; + assert_eq!( + settings.node_launchpad_path, + Some(node_launchpad_bin_file.to_path_buf()) + ); + assert_eq!(settings.node_launchpad_version, Some(Version::new(0, 2, 0))); assert_eq!(settings.safe_path, Some(safe_bin_file.to_path_buf())); assert_eq!(settings.safe_version, Some(Version::new(0, 75, 1))); assert_eq!( @@ -837,10 +866,12 @@ mod test { safenode_bin_file.write_binary(b"fake safenode code")?; let safenode_manager_bin_file = tmp_data_path.child("safenode-manager"); safenode_manager_bin_file.write_binary(b"fake safenode-manager code")?; - let testnet_bin_file = tmp_data_path.child("testnet"); - testnet_bin_file.write_binary(b"fake testnet code")?; + let node_launchpad_bin_file = tmp_data_path.child("node-launchpad"); + node_launchpad_bin_file.write_binary(b"fake launchpad code")?; let settings = Settings { + node_launchpad_path: Some(node_launchpad_bin_file.to_path_buf()), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(safe_bin_file.to_path_buf()), safe_version: Some(Version::new(0, 75, 1)), safenode_path: Some(safenode_bin_file.to_path_buf()), diff --git a/src/main.rs b/src/main.rs index a7ad6f0..45f0112 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,6 +77,30 @@ enum Commands { #[arg(short = 'v', long)] version: Option, }, + /// Install the node-launchpad binary. + /// + /// The location is platform specific: + /// - Linux/macOS: $HOME/.local/bin + /// - Windows: C:\Users\\safe + /// + /// On Linux/macOS, the Bash shell profile will be modified to add $HOME/.local/bin to the PATH + /// variable. On Windows, the user Path variable will be modified to add C:\Users\\safe. + #[clap(verbatim_doc_comment)] + NodeLaunchpad { + /// Override the default installation path. + /// + /// Any directories that don't exist will be created. + #[arg(short = 'p', long, value_name = "DIRECTORY")] + path: Option, + + /// Disable modification of the shell profile. + #[arg(short = 'n', long)] + no_modify_shell_profile: bool, + + /// Install a specific version rather than the latest. + #[arg(short = 'v', long)] + version: Option, + }, /// Install the safenode-manager binary. /// /// The location is platform specific: @@ -139,6 +163,25 @@ async fn main() -> Result<()> { install::check_prerequisites()?; process_install_cmd(AssetType::Node, path, version, no_modify_shell_profile).await } + Some(Commands::NodeLaunchpad { + path, + no_modify_shell_profile, + version, + }) => { + println!("**************************************"); + println!("* *"); + println!("* Installing node-launchpad *"); + println!("* *"); + println!("**************************************"); + install::check_prerequisites()?; + process_install_cmd( + AssetType::NodeLaunchpad, + path, + version, + no_modify_shell_profile, + ) + .await + } Some(Commands::NodeManager { path, no_modify_shell_profile, diff --git a/src/update.rs b/src/update.rs index 2e133f8..9879203 100644 --- a/src/update.rs +++ b/src/update.rs @@ -51,6 +51,8 @@ mod test { #[test] fn perform_upgrade_assessment_should_indicate_no_previous_installation() -> Result<()> { let settings = Settings { + node_launchpad_path: Some(PathBuf::from("/home/chris/.local/bin/node-launchpad")), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: None, safe_version: None, safenode_path: Some(PathBuf::from("/home/chris/.local/bin/safenode")), @@ -63,6 +65,8 @@ mod test { assert_matches!(decision, UpdateAssessmentResult::NoPreviousInstallation); let settings = Settings { + node_launchpad_path: Some(PathBuf::from("/home/chris/.local/bin/node-launchpad")), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(PathBuf::from("/home/chris/.local/safe")), safe_version: Some(Version::new(0, 78, 26)), safenode_path: None, @@ -75,6 +79,8 @@ mod test { assert_matches!(decision, UpdateAssessmentResult::NoPreviousInstallation); let settings = Settings { + node_launchpad_path: Some(PathBuf::from("/home/chris/.local/bin/node-launchpad")), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(PathBuf::from("/home/chris/.local/safe")), safe_version: Some(Version::new(0, 78, 26)), safenode_path: Some(PathBuf::from("/home/chris/.local/bin/safenode")), @@ -86,12 +92,31 @@ mod test { perform_update_assessment(&AssetType::NodeManager, &Version::new(0, 1, 8), &settings)?; assert_matches!(decision, UpdateAssessmentResult::NoPreviousInstallation); + let settings = Settings { + node_launchpad_path: None, + node_launchpad_version: None, + safe_path: Some(PathBuf::from("/home/chris/.local/safe")), + safe_version: Some(Version::new(0, 78, 26)), + safenode_path: Some(PathBuf::from("/home/chris/.local/bin/safenode")), + safenode_version: Some(Version::new(0, 83, 13)), + safenode_manager_path: Some(PathBuf::from("/home/chris/.local/bin/safenode-manager")), + safenode_manager_version: Some(Version::new(0, 1, 8)), + }; + let decision = perform_update_assessment( + &AssetType::NodeLaunchpad, + &Version::new(0, 2, 0), + &settings, + )?; + assert_matches!(decision, UpdateAssessmentResult::NoPreviousInstallation); + Ok(()) } #[test] fn perform_upgrade_assessment_should_indicate_we_are_at_latest_version() -> Result<()> { let settings = Settings { + node_launchpad_path: Some(PathBuf::from("/home/chris/.local/node-launchpad")), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(PathBuf::from("/home/chris/.local/safe")), safe_version: Some(Version::new(0, 78, 26)), safenode_path: Some(PathBuf::from("/home/chris/.local/bin/safenode")), @@ -112,6 +137,13 @@ mod test { perform_update_assessment(&AssetType::NodeManager, &Version::new(0, 1, 8), &settings)?; assert_matches!(decision, UpdateAssessmentResult::AtLatestVersion); + let decision = perform_update_assessment( + &AssetType::NodeLaunchpad, + &Version::new(0, 2, 0), + &settings, + )?; + assert_matches!(decision, UpdateAssessmentResult::AtLatestVersion); + Ok(()) } @@ -119,6 +151,8 @@ mod test { fn perform_upgrade_assessment_latest_version_is_less_than_current_should_return_error( ) -> Result<()> { let settings = Settings { + node_launchpad_path: Some(PathBuf::from("/home/chris/.local/node-launchpad")), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(PathBuf::from("/home/chris/.local/safe")), safe_version: Some(Version::new(0, 78, 26)), safenode_path: Some(PathBuf::from("/home/chris/.local/bin/safenode")), @@ -157,6 +191,16 @@ mod test { ), } + let result = + perform_update_assessment(&AssetType::NodeLaunchpad, &Version::new(0, 1, 0), &settings); + match result { + Ok(_) => return Err(eyre!("this test should return an error")), + Err(e) => assert_eq!( + "The latest version is less than the current version of your binary.", + e.to_string() + ), + } + Ok(()) } @@ -164,6 +208,8 @@ mod test { fn perform_upgrade_assessment_should_perform_update_when_latest_patch_version_is_greater( ) -> Result<()> { let settings = Settings { + node_launchpad_path: Some(PathBuf::from("/home/chris/.local/node-launchpad")), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(PathBuf::from("/home/chris/.local/safe")), safe_version: Some(Version::new(0, 78, 26)), safenode_path: Some(PathBuf::from("/home/chris/.local/bin/safenode")), @@ -184,6 +230,13 @@ mod test { perform_update_assessment(&AssetType::NodeManager, &Version::new(0, 1, 8), &settings)?; assert_matches!(decision, UpdateAssessmentResult::PerformUpdate(_)); + let decision = perform_update_assessment( + &AssetType::NodeLaunchpad, + &Version::new(0, 2, 1), + &settings, + )?; + assert_matches!(decision, UpdateAssessmentResult::PerformUpdate(_)); + Ok(()) } @@ -191,6 +244,8 @@ mod test { fn perform_upgrade_assessment_should_perform_update_when_latest_minor_version_is_greater( ) -> Result<()> { let settings = Settings { + node_launchpad_path: Some(PathBuf::from("/home/chris/.local/node-launchpad")), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(PathBuf::from("/home/chris/.local/safe")), safe_version: Some(Version::new(0, 78, 26)), safenode_path: Some(PathBuf::from("/home/chris/.local/bin/safenode")), @@ -211,6 +266,13 @@ mod test { perform_update_assessment(&AssetType::NodeManager, &Version::new(0, 2, 0), &settings)?; assert_matches!(decision, UpdateAssessmentResult::PerformUpdate(_)); + let decision = perform_update_assessment( + &AssetType::NodeLaunchpad, + &Version::new(0, 3, 0), + &settings, + )?; + assert_matches!(decision, UpdateAssessmentResult::PerformUpdate(_)); + Ok(()) } @@ -218,6 +280,8 @@ mod test { fn perform_upgrade_assessment_should_perform_update_when_latest_major_version_is_greater( ) -> Result<()> { let settings = Settings { + node_launchpad_path: Some(PathBuf::from("/home/chris/.local/node-launchpad")), + node_launchpad_version: Some(Version::new(0, 2, 0)), safe_path: Some(PathBuf::from("/home/chris/.local/safe")), safe_version: Some(Version::new(0, 78, 26)), safenode_path: Some(PathBuf::from("/home/chris/.local/bin/safenode")), @@ -238,6 +302,13 @@ mod test { perform_update_assessment(&AssetType::NodeManager, &Version::new(1, 0, 0), &settings)?; assert_matches!(decision, UpdateAssessmentResult::PerformUpdate(_)); + let decision = perform_update_assessment( + &AssetType::NodeLaunchpad, + &Version::new(1, 0, 0), + &settings, + )?; + assert_matches!(decision, UpdateAssessmentResult::PerformUpdate(_)); + Ok(()) } }