Repository: comtrya/comtrya (language: rust)

Issue:
New Action: Install GitHub Release Binaries



Discussion / hints:
+1 for this one, with my first target installing the Equinix Metal (Packet) CLI from https://github.com/packethost/packet-cli/releases



Fix patch:
diff --git a/examples/binary/github.yaml b/examples/binary/github.yaml
new file mode 100644
index 00000000..2ad563d0
--- /dev/null
+++ b/examples/binary/github.yaml
@@ -0,0 +1,5 @@
+actions:
+  - action: binary.github
+    name: comtrya
+    directory: /tmp
+    repository: comtrya/comtrya
diff --git a/lib/Cargo.toml b/lib/Cargo.toml
index 5ae8a809..208d277c 100644
--- a/lib/Cargo.toml
+++ b/lib/Cargo.toml
@@ -31,6 +31,7 @@ serde = { version = "1.0", features = ["derive"] }
 serde_json = "1.0.81"
 serde_yaml = "0.8.24"
 structopt = { version = "0.3", features = ["paw"] }
+tokio = "1.20.1"
 tera = "1"
 tracing = "0.1.34"
 tracing-subscriber = "0.3.11"
@@ -41,6 +42,7 @@ sha256 = "1.0.3"
 schemars = "0.8.10"
 trust-dns-resolver = "0.21.2"
 toml = "0.5.9"
+octocrab = "0.16.0"
 
 [target.'cfg(unix)'.dependencies]
 users = "0.11.0"
diff --git a/lib/src/actions/binary/github.rs b/lib/src/actions/binary/github.rs
new file mode 100644
index 00000000..93fc1b68
--- /dev/null
+++ b/lib/src/actions/binary/github.rs
@@ -0,0 +1,144 @@
+use crate::actions::Action;
+use crate::atoms::file::Chmod;
+use crate::atoms::http::Download;
+use crate::contexts::Contexts;
+use crate::manifests::Manifest;
+use crate::steps::Step;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::path::PathBuf;
+use tokio::runtime::Runtime;
+use tracing::{debug, error};
+
+#[derive(Clone, Debug, Default, JsonSchema, PartialEq, Serialize, Deserialize)]
+pub struct BinaryGitHub {
+    pub name: String,
+    pub directory: String,
+    pub repository: String,
+    pub version: Option<String>,
+}
+
+struct GitHubAsset {
+    pub url: String,
+    pub score: i32,
+}
+
+impl Action for BinaryGitHub {
+    fn plan(&self, _: &Manifest, _: &Contexts) -> Vec<Step> {
+        // Don't need to do anything if something already exists at the path
+        if (std::path::Path::new(format!("{}/{}", self.directory, self.name).as_str()).exists()) {
+            return vec![];
+        };
+
+        let async_runtime = match Runtime::new() {
+            Ok(runtime) => runtime,
+            Err(e) => {
+                error!("Failed to create async runtime: {}", e);
+                return vec![];
+            }
+        };
+
+        let (owner, repo) = self.repository.split_once("/").unwrap();
+
+        let octocrab = octocrab::instance();
+
+        let repos = octocrab.repos(owner, repo);
+        let releases = repos.releases();
+
+        let result = match &self.version {
+            Some(version) => async_runtime.block_on(releases.get_by_tag(version.as_str())),
+            None => async_runtime.block_on(releases.get_latest()),
+        };
+
+        let release = match result {
+            Ok(release) => release,
+            Err(e) => {
+                error!("Failed to find a release: {}", e);
+                return vec![];
+            }
+        };
+
+        let asset: Option<GitHubAsset> = release.assets.into_iter().fold(None, |acc, asset| {
+            let mut score = 0;
+
+            let mut score_terms = vec![
+                std::env::consts::OS.to_lowercase(),
+                std::env::consts::ARCH.to_lowercase(),
+            ];
+
+            let os = os_info::get();
+            if os.os_type() == os_info::Type::Macos {
+                score_terms.push(String::from("darwin"));
+                score_terms.push(String::from("apple"));
+            } else {
+                score_terms.push(os.os_type().to_string());
+            };
+
+            if std::env::consts::ARCH == "aarch64" {
+                score_terms.push("arm".to_string());
+                score_terms.push("aarch".to_string());
+            } else {
+                score_terms.push("unknown".to_string());
+            };
+
+            match os.bitness() {
+                os_info::Bitness::X32 => score_terms.push("32".to_string()),
+                os_info::Bitness::X64 => score_terms.push("64".to_string()),
+                _ => (),
+            }
+
+            score_terms.iter().for_each(|term| {
+                if asset.name.to_lowercase().contains(term.as_str()) {
+                    score += 1;
+                }
+            });
+
+            match acc {
+                Some(ass) => {
+                    if score > ass.score {
+                        Some(GitHubAsset {
+                            url: asset.browser_download_url.into(),
+                            score: score,
+                        })
+                    } else {
+                        Some(ass)
+                    }
+                }
+                None => Some(GitHubAsset {
+                    url: asset.browser_download_url.into(),
+                    score: score,
+                }),
+            }
+        });
+
+        let asset = match asset {
+            Some(asset) => {
+                debug!("Downloading {:?}", asset.url);
+                asset
+            }
+            None => {
+                error!("Failed to find a downloadable asset");
+                return vec![];
+            }
+        };
+
+        vec![
+            Step {
+                atom: Box::new(Download {
+                    url: asset.url,
+                    to: PathBuf::from(format!("{}/{}", self.directory, self.name)),
+                }),
+                initializers: vec![],
+                finalizers: vec![],
+            },
+            Step {
+                atom: Box::new(Chmod {
+                    path: PathBuf::from(format!("{}/{}", self.directory, self.name)),
+                    mode: 0o755,
+                }),
+                initializers: vec![],
+                finalizers: vec![],
+            },
+        ]
+    }
+}
diff --git a/lib/src/actions/binary/mod.rs b/lib/src/actions/binary/mod.rs
new file mode 100644
index 00000000..4e962dd1
--- /dev/null
+++ b/lib/src/actions/binary/mod.rs
@@ -0,0 +1,3 @@
+mod github;
+
+pub use github::BinaryGitHub;
diff --git a/lib/src/actions/mod.rs b/lib/src/actions/mod.rs
index 06314913..f59665fa 100644
--- a/lib/src/actions/mod.rs
+++ b/lib/src/actions/mod.rs
@@ -1,3 +1,4 @@
+mod binary;
 mod command;
 mod directory;
 mod file;
@@ -9,6 +10,7 @@ mod user;
 use crate::contexts::Contexts;
 use crate::manifests::Manifest;
 use crate::steps::Step;
+use binary::BinaryGitHub;
 use command::run::RunCommand;
 use directory::{DirectoryCopy, DirectoryCreate};
 use file::copy::FileCopy;
@@ -111,6 +113,14 @@ pub enum Actions {
     #[serde(rename = "file.link")]
     FileLink(ConditionalVariantAction<FileLink>),
 
+    #[serde(
+        rename = "binary.github",
+        alias = "binary.gh",
+        alias = "bin.github",
+        alias = "bin.gh"
+    )]
+    BinaryGitHub(ConditionalVariantAction<BinaryGitHub>),
+
     #[serde(rename = "git.clone")]
     GitClone(ConditionalVariantAction<GitClone>),
 
@@ -130,6 +140,7 @@ pub enum Actions {
 impl Actions {
     pub fn inner_ref(&self) -> &dyn Action {
         match self {
+            Actions::BinaryGitHub(a) => a,
             Actions::CommandRun(a) => a,
             Actions::DirectoryCopy(a) => a,
             Actions::DirectoryCreate(a) => a,
@@ -154,6 +165,7 @@ impl Display for Actions {
             Actions::FileCopy(_) => "file.copy",
             Actions::FileDownload(_) => "file.download",
             Actions::FileLink(_) => "file.link",
+            Actions::BinaryGitHub(_) => "github.binary",
             Actions::GitClone(_) => "git.clone",
             Actions::MacOSDefault(_) => "macos.default",
             Actions::PackageInstall(_) => "package.install",


Test patch: