From a17a23e47f6d92cf3e37c3c7532c794aa1934406 Mon Sep 17 00:00:00 2001 From: Yonas Date: Fri, 27 Jun 2025 11:34:58 -0400 Subject: [PATCH] feat: Add doctor diagnostics CLI command. --- .gitignore | 3 + backpack/cli/Cargo.toml | 3 + backpack/cli/build.rs | 19 +++ backpack/cli/src/main.rs | 43 ++++--- backpack/justfile | 9 ++ backpack/lib/Cargo.toml | 17 ++- backpack/lib/build.rs | 27 ++++ backpack/lib/src/lib.rs | 270 ++++++++++++++++++++++++++++++++++++++- deny.toml | 1 + 9 files changed, 366 insertions(+), 26 deletions(-) create mode 100644 backpack/cli/build.rs create mode 100644 backpack/justfile create mode 100644 backpack/lib/build.rs diff --git a/.gitignore b/.gitignore index e5c1e32..b5401cf 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,6 @@ Cargo.lock # Cargo about *.hbs + +# Core dump +*.core diff --git a/backpack/cli/Cargo.toml b/backpack/cli/Cargo.toml index 9720ec3..30fefd3 100644 --- a/backpack/cli/Cargo.toml +++ b/backpack/cli/Cargo.toml @@ -27,6 +27,9 @@ coverage = [] [dev-dependencies] test-log = { version = "0.2.17", features = ["trace", "color"] } +[build-dependencies] +vergen = { version = "9.0.6", features = ["time", "cargo", "build", "rustc", "si"] } + [package.metadata.binstall.signing] algorithm = "minisign" pubkey = "RWS6/A1iiYtBjU101ofgB5ZBUq+erhj0pAF06delVbHPUiDee7PQvIML" diff --git a/backpack/cli/build.rs b/backpack/cli/build.rs new file mode 100644 index 0000000..3b89db4 --- /dev/null +++ b/backpack/cli/build.rs @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2025 yonasBSD +// +// SPDX-License-Identifier: MIT + +use vergen::{BuildBuilder, CargoBuilder, Emitter, RustcBuilder}; + +fn main() -> Result<(), Box> { + let build = BuildBuilder::all_build()?; + let cargo = CargoBuilder::all_cargo()?; + let rustc = RustcBuilder::all_rustc()?; + + Emitter::default() + .add_instructions(&build)? + .add_instructions(&cargo)? + .add_instructions(&rustc)? + .emit()?; + + Ok(()) +} diff --git a/backpack/cli/src/main.rs b/backpack/cli/src/main.rs index 6884867..2f960ea 100644 --- a/backpack/cli/src/main.rs +++ b/backpack/cli/src/main.rs @@ -5,7 +5,7 @@ #[cfg(not(feature = "coverage"))] use clap::Parser; -use github_rs_lib::{Cli, get_repos, get_token, update_repos}; +use github_rs_lib::{Cli, Commands, doctor, get_repos, get_token, update_repos}; use std::error::Error; use terminal_banner::Banner; use tracing_subscriber::{Registry, fmt, prelude::*}; @@ -71,24 +71,29 @@ async fn main() -> Result<(), Box> { "Parsed command line arguments" ); - if cli.sync { - tracing::warn!("Sync enabled. This might take a while."); - } - - let token = get_token(cli.token.unwrap_or_default()).await?; - tracing::trace!(token = token, "Got GitHub token"); - - // Get the value of the positional argument (if provided) - let repos = match cli.org { - Some(org) => get_repos(token.clone(), Some(org)).await?, - None => get_repos(token.clone(), None).await?, - }; - - let count = update_repos(repos, cli.sync, token.clone()).await?; - tracing::trace!(count = count, "Got count of GitHub repos updated"); - - if count > 0 { - println!("Total updates: {count}"); + match &cli.command { + Some(Commands::Doctor {}) => doctor().await.expect("Run doctor"), + None => { + if cli.sync { + tracing::warn!("Sync enabled. This might take a while."); + } + + let token = get_token(cli.token.unwrap_or_default()).await?; + tracing::trace!(token = token, "Got GitHub token"); + + // Get the value of the positional argument (if provided) + let repos = match cli.org { + Some(org) => get_repos(token.clone(), Some(org)).await?, + None => get_repos(token.clone(), None).await?, + }; + + let count = update_repos(repos, cli.sync, token.clone()).await?; + tracing::trace!(count = count, "Got count of GitHub repos updated"); + + if count > 0 { + println!("Total updates: {count}"); + } + } } Ok(()) diff --git a/backpack/justfile b/backpack/justfile new file mode 100644 index 0000000..0bbbe0d --- /dev/null +++ b/backpack/justfile @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 yonasBSD +# +# SPDX-License-Identifier: MIT + +default: build + +@build: + clear + cargo build --release --package github-rs && ../target/release/github-rs doctor diff --git a/backpack/lib/Cargo.toml b/backpack/lib/Cargo.toml index 8945787..ba73fb9 100644 --- a/backpack/lib/Cargo.toml +++ b/backpack/lib/Cargo.toml @@ -6,7 +6,12 @@ name = "github-rs-lib" version = "0.1.0" edition = "2024" license = "MIT" +authors = ["Yonas Yanfa", "Yonas Yanfa "] +description = "Automatically update all your forked repositories on Github." +homepage = "https://github.com/yonasBSD/github-rs" repository = "https://github.com/yonasBSD/github-rs" +keywords = ["github", "git"] +categories = ["github", "git"] [features] coverage = [] @@ -20,11 +25,19 @@ which = "8.0.0" config = "0.15.4" xdg = "3.0.0" reqwest = { version = "0.12.9", default-features = false, features = ["blocking", "hickory-dns", "json", "rustls-tls"] } -env_logger = "0.11.5" +#env_logger = "0.11.5" tracing = { version = "0.1.41", features = ["log"] } tracing-subscriber = { version = "0.3.19", features = ["env-filter", "json"] } terminal-banner = { version = "0.4.1", features = ["color"] } -serde_json5 = "0.2.1" +dns-lookup = "2.0.4" +rdap_client = "0.2.0" +taplo = { version = "0.14.0", features = ["schema", "schemars"] } +uuid = { version = "1.17.0", features = ["v4"] } [dev-dependencies] test-log = { version = "0.2.17", features = ["trace", "color"] } +serde_json5 = "0.2.1" + +[build-dependencies] +build-data = "0.3.3" +vergen = { version = "9.0.6", features = ["time", "cargo", "build", "rustc", "si"] } diff --git a/backpack/lib/build.rs b/backpack/lib/build.rs new file mode 100644 index 0000000..aef8614 --- /dev/null +++ b/backpack/lib/build.rs @@ -0,0 +1,27 @@ +/* + * SPDX-FileCopyrightText: 2025 yonasBSD + * + * SPDX-License-Identifier: MIT + */ + +use vergen::{BuildBuilder, CargoBuilder, Emitter, RustcBuilder}; + +fn main() -> Result<(), Box> { + let build = BuildBuilder::all_build()?; + let cargo = CargoBuilder::all_cargo()?; + let rustc = RustcBuilder::all_rustc()?; + + Emitter::default() + .add_instructions(&build)? + .add_instructions(&cargo)? + .add_instructions(&rustc)? + .emit()?; + + build_data::set_GIT_BRANCH().unwrap(); + build_data::set_GIT_COMMIT().unwrap(); + build_data::set_GIT_DIRTY().unwrap(); + build_data::set_SOURCE_TIMESTAMP().unwrap(); + build_data::no_debug_rebuilds().unwrap(); + + Ok(()) +} diff --git a/backpack/lib/src/lib.rs b/backpack/lib/src/lib.rs index 079507a..01bf8f9 100644 --- a/backpack/lib/src/lib.rs +++ b/backpack/lib/src/lib.rs @@ -3,18 +3,22 @@ #![feature(coverage_attribute)] -use clap::Parser; +use clap::{Parser, Subcommand}; use colored::Colorize; +use config::Config; use octocrab::Octocrab; use octocrab::models::Repository; use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE, HeaderMap, HeaderValue, USER_AGENT}; -use std::{collections::HashMap, error::Error}; +use std::{collections::HashMap, error::Error, fmt, fs}; +use terminal_banner::{Banner, Text, TextAlign}; use tracing::Level; use which::which; /// Automatically update all your forked repositories on Github -#[derive(Parser)] +#[derive(Debug, Parser)] #[clap(version)] +#[command(author, version, about, long_about = None)] +#[command(propagate_version = true)] pub struct Cli { /// The organization #[arg(index = 1)] @@ -27,6 +31,25 @@ pub struct Cli { /// GitHub token #[arg(short = 't', long = "token")] pub token: Option, + + /// Command + #[command(subcommand)] + pub command: Option, + //Version(clap_vergen::Version), +} + +#[derive(Debug, Subcommand)] +pub enum Commands { + /// Doctor diagnostics + Doctor {}, +} + +impl fmt::Display for Commands { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Commands::Doctor {} => write!(f, "Doctor command"), + } + } } /// Multiplies two integers @@ -34,6 +57,243 @@ pub fn multiply(a: i32, b: i32) -> i32 { a * b } +/// Doctor build information +pub fn doctor_build() { + println!("{}", "# Build Information\n".yellow()); + const VERSION: &str = env!("CARGO_PKG_VERSION"); + const AUTHORS: &str = env!("CARGO_PKG_AUTHORS"); + const DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); + const HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); + const REPOSITORY: &str = env!("CARGO_PKG_REPOSITORY"); + + use uuid::Uuid; + let uuid = Uuid::new_v4(); + + println!("{}: {}", "Version".green(), VERSION.purple()); + println!("{}: {}", "Build ID".green(), uuid.to_string().purple()); + println!( + "{}: {}", + "Build Date".green(), + env!("VERGEN_RUSTC_COMMIT_DATE").to_string().purple() + ); + println!( + "{} {}{} {}{} {}{} {}{}", + "Built from".green(), + "branch=".green(), + env!("GIT_BRANCH").purple(), + "commit=".green(), + env!("GIT_COMMIT").purple(), + "dirty=".green(), + env!("GIT_DIRTY").purple(), + "source_timestamp=".green(), + env!("SOURCE_TIMESTAMP").purple() + ); + + println!("{}: {}", "Authors".green(), AUTHORS.purple()); + println!("{}: {}", "Description".green(), DESCRIPTION.purple()); + println!("{}: {}", "Homepage".green(), HOMEPAGE.purple()); + println!("{}: {}", "Repositories".green(), REPOSITORY.purple()); +} + +/// Doctor config +pub fn doctor_config() -> Result<(), Box> { + println!("{}", "\n# Config\n".yellow()); + + let xdg_path = xdg::BaseDirectories::with_prefix("") + .get_config_home() + .unwrap(); + + println!( + "{}: {}", + "✓ found XDG config path".green(), + xdg_path.to_str().unwrap().purple() + ); + + let config_path = xdg::BaseDirectories::with_prefix("github-rs") + .get_config_home() + .unwrap(); + + println!( + "{}: {}", + "✓ found github-rs config path".green(), + config_path.to_str().unwrap().purple() + ); + + let file: String = + fs::read_to_string(format!("{}/{}", config_path.display(), "config.toml").as_str())?; + let parse_result = taplo::parser::parse(&file); + + if parse_result.errors.is_empty() { + println!( + "{}: {}", + "✓ found valid TOML config file at".green(), + format!("{}/{}", config_path.display(), "config.toml") + .as_str() + .purple(), + ); + } else { + println!( + "{}: {}", + "[ ! ] invalid TOML file at".red(), + format!("{}/{}", config_path.display(), "config.toml") + .as_str() + .purple(), + ); + } + + Ok(()) +} + +/// Doctor token +pub fn doctor_token() -> String { + let config_path = xdg::BaseDirectories::with_prefix("github-rs") + .get_config_home() + .unwrap(); + + let token = match Config::builder() + // ~/.config/github-rs/config.toml + .add_source(config::File::with_name( + format!("{}/{}", config_path.display(), "config").as_str(), + )) + // env variables + .add_source(config::Environment::with_prefix("GITHUB")) + .build() + { + Ok(settings) => { + println!("{}", "\n# Token\n".yellow()); + + // Read config file + let app = settings + .try_deserialize::>() + .unwrap(); + + if app.contains_key("token") { + println!( + "{}: {}", + "✓ found GitHub token".green(), + app["token"].clone().purple() + ); + app["token"].clone() + } else { + println!("{}", "\n# Token\n".yellow()); + eprintln!("{}", "[ ! ] could not find GitHub token".red()); + String::from("") + } + } + + Err(_) => { + println!("{}", "\n# Token\n".yellow()); + eprintln!("{}", "[ ! ] Error: Could not find GitHub token".red()); + String::from("") + } + }; + + token +} + +/// Doctor network +pub async fn doctor_network() { + println!("{}", "\n# Network\n".yellow()); + + use dns_lookup::lookup_host; + + let hostname = "github.com"; + let ips: Vec = lookup_host(hostname).unwrap(); + if !ips.is_empty() { + for ip in ips { + println!( + "{} {}: {}", + "✓ found IP address for".green(), + hostname.green(), + ip.to_string().purple() + ); + } + } else { + println!( + "{} {}", + "[ ! ] Unable to find IP address for".red(), + hostname.red() + ); + } + + use rdap_client::Client; + + let client = Client::new(); + // Fetch boostrap from IANA. + let bootstrap = client.fetch_bootstrap().await.unwrap(); + // Find what RDAP server to use for given domain. + if let Some(servers) = bootstrap.dns.find(&hostname) { + let response = client.query_domain(&servers[0], hostname).await.unwrap(); + println!( + "{} {}: {}", + "✓ found domain registration for".green(), + hostname.green(), + response.handle.expect("Bad response").to_string().purple() + ); + } +} + +/// Doctor security +pub async fn doctor_security(token: String) -> Result<(), Box> { + println!("{}", "\n# Security\n".yellow()); + let octocrab = Octocrab::builder().personal_token(token.clone()).build()?; + let user = octocrab.current().user().await?; + + if !token.is_empty() { + if !user.login.is_empty() { + println!("{}", "✓ found correct permissions on GitHub token".green()); + } else { + println!("{}", "[ ! ] invalid permissions on GitHub token".red()); + } + } + + Ok(()) +} + +/// Doctor diagnostics +pub async fn doctor() -> Result<(), Box> { + tracing::event!(Level::TRACE, "Calling doctor()"); + let banner = Banner::new() + .text(Text::from("Doctor Diagnostics").align(TextAlign::Center)) + .render(); + println!("{}", banner.cyan()); + println!( + "{}", + " + // 1. Build Information + // 1.1 version + // 1.2 build id + // 1.3 build date + // + // 2. Config + // 2.1 XDG Config directory exists + // 2.2 github-rs config directory exists + // 2.3 github-rs config file exists + // + // 3. Tokens + // 3.1 GITHUB_TOKEN env variable exists + // 3.2 token found in config file + / + // 4. Network + // 4.1 github.com is registered (DNS) + // 4.1 github.com is resolvable (DNS) + // 4.2 GitHub API is operational (API) + // + // 5. Security + // 5.1 GitHub token is valid + " + .cyan() + ); + + doctor_build(); + let _ = doctor_config(); + let token = doctor_token(); + doctor_network().await; + let _ = doctor_security(token); + + Ok(()) +} + /// List GitHub repositories /// # Panics /// @@ -129,14 +389,14 @@ pub async fn get_token(token: String) -> Result> { app["token"].clone() } else { tracing::error!("could not find GitHub token"); - println!("{}", "Error: Could not find GitHub token".red()); + println!("{}", "[ ! ] Error: Could not find GitHub token".red()); std::process::exit(1) } } Err(_) => { tracing::error!("could not find GitHub token"); - println!("{}", "Error: Could not find GitHub token".red()); + println!("{}", "[ ! ] Error: Could not find GitHub token".red()); std::process::exit(1); } }; diff --git a/deny.toml b/deny.toml index 976926f..dac82a6 100644 --- a/deny.toml +++ b/deny.toml @@ -105,6 +105,7 @@ allow = [ "Apache-2.0", "MPL-2.0", "Unicode-3.0", + "BSD-2-Clause", "BSD-3-Clause", "Zlib", "ISC",