mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-08-28 05:27:41 +00:00
parent
e0214f5ed4
commit
31c973ceed
5 changed files with 463 additions and 8 deletions
File diff suppressed because one or more lines are too long
425
nodes/simplify/diff.patch
Normal file
425
nodes/simplify/diff.patch
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
diff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml
|
||||
index 7e32305..03d5597 100644
|
||||
--- a/lib/crates/fabro-cli/Cargo.toml
|
||||
+++ b/lib/crates/fabro-cli/Cargo.toml
|
||||
@@ -70,4 +70,4 @@ insta = { workspace = true }
|
||||
predicates = "3"
|
||||
serde_json.workspace = true
|
||||
httpmock = "0.8"
|
||||
-trycmd = "0.15"
|
||||
\ No newline at end of file
|
||||
+trycmd = "0.15"
|
||||
diff --git a/lib/crates/fabro-cli/src/main.rs b/lib/crates/fabro-cli/src/main.rs
|
||||
index 97fdfb5..8c73773 100644
|
||||
--- a/lib/crates/fabro-cli/src/main.rs
|
||||
+++ b/lib/crates/fabro-cli/src/main.rs
|
||||
@@ -433,17 +433,17 @@ async fn main_inner() -> (String, Result<()>) {
|
||||
|
||||
let command_name = command_name.to_string();
|
||||
|
||||
- let config_log_level = {
|
||||
+ let (config_log_level, upgrade_check_enabled) = {
|
||||
#[cfg(feature = "server")]
|
||||
{
|
||||
if let Command::Serve(ref args) = cli.command {
|
||||
match fabro_config::server::load_server_config(args.config.as_deref()) {
|
||||
- Ok(server_config) => server_config.log.level,
|
||||
+ Ok(server_config) => (server_config.log.level, false),
|
||||
Err(err) => return (command_name, Err(err)),
|
||||
}
|
||||
} else {
|
||||
match fabro_config::cli::load_cli_config(None) {
|
||||
- Ok(cli_config) => cli_config.log.level,
|
||||
+ Ok(cli_config) => (cli_config.log.level, cli_config.upgrade_check),
|
||||
Err(err) => return (command_name, Err(err)),
|
||||
}
|
||||
}
|
||||
@@ -451,7 +451,7 @@ async fn main_inner() -> (String, Result<()>) {
|
||||
#[cfg(not(feature = "server"))]
|
||||
{
|
||||
match fabro_config::cli::load_cli_config(None) {
|
||||
- Ok(cli_config) => cli_config.log.level,
|
||||
+ Ok(cli_config) => (cli_config.log.level, cli_config.upgrade_check),
|
||||
Err(err) => return (command_name, Err(err)),
|
||||
}
|
||||
}
|
||||
@@ -468,13 +468,14 @@ async fn main_inner() -> (String, Result<()>) {
|
||||
|
||||
debug!(command = %command_name, "CLI command started");
|
||||
|
||||
- let check_upgrade = matches!(
|
||||
+ let upgrade_handle = if matches!(
|
||||
cli.command,
|
||||
Command::Run(_) | Command::Exec(_) | Command::Init | Command::Install
|
||||
- );
|
||||
- if check_upgrade {
|
||||
- upgrade::maybe_print_upgrade_notice(cli.no_upgrade_check).await;
|
||||
- }
|
||||
+ ) {
|
||||
+ upgrade::spawn_upgrade_check(cli.no_upgrade_check, upgrade_check_enabled)
|
||||
+ } else {
|
||||
+ None
|
||||
+ };
|
||||
|
||||
let result = async {
|
||||
match cli.command {
|
||||
@@ -787,5 +788,10 @@ async fn main_inner() -> (String, Result<()>) {
|
||||
}
|
||||
.await;
|
||||
|
||||
+ // Print upgrade notice after command completes (non-blocking during execution)
|
||||
+ if let Some(handle) = upgrade_handle {
|
||||
+ let _ = handle.await;
|
||||
+ }
|
||||
+
|
||||
(command_name, result)
|
||||
}
|
||||
diff --git a/lib/crates/fabro-cli/src/upgrade.rs b/lib/crates/fabro-cli/src/upgrade.rs
|
||||
index e5753b1..f3c9e98 100644
|
||||
--- a/lib/crates/fabro-cli/src/upgrade.rs
|
||||
+++ b/lib/crates/fabro-cli/src/upgrade.rs
|
||||
@@ -26,9 +26,18 @@ pub struct UpgradeArgs {
|
||||
|
||||
// ── Download backend abstraction ───────────────────────────────────────────
|
||||
|
||||
+const GITHUB_REPO: &str = "fabro-sh/fabro";
|
||||
+
|
||||
enum Backend {
|
||||
Gh,
|
||||
- Http,
|
||||
+ Http(reqwest::Client),
|
||||
+}
|
||||
+
|
||||
+fn http_client() -> Result<reqwest::Client> {
|
||||
+ reqwest::Client::builder()
|
||||
+ .user_agent("fabro-cli")
|
||||
+ .build()
|
||||
+ .context("failed to build HTTP client")
|
||||
}
|
||||
|
||||
impl Backend {
|
||||
@@ -40,7 +49,7 @@ impl Backend {
|
||||
"release",
|
||||
"view",
|
||||
"--repo",
|
||||
- "fabro-sh/fabro",
|
||||
+ GITHUB_REPO,
|
||||
"--json",
|
||||
"tagName",
|
||||
"-q",
|
||||
@@ -55,10 +64,10 @@ impl Backend {
|
||||
}
|
||||
Ok(String::from_utf8(output.stdout)?.trim().to_string())
|
||||
}
|
||||
- Backend::Http => {
|
||||
- let client = reqwest::Client::builder().user_agent("fabro-cli").build()?;
|
||||
+ Backend::Http(client) => {
|
||||
+ let url = format!("https://api.github.com/repos/{GITHUB_REPO}/releases/latest");
|
||||
let resp = client
|
||||
- .get("https://api.github.com/repos/fabro-sh/fabro/releases/latest")
|
||||
+ .get(&url)
|
||||
.send()
|
||||
.await
|
||||
.context("failed to fetch latest release from GitHub API")?;
|
||||
@@ -87,7 +96,7 @@ impl Backend {
|
||||
"download",
|
||||
tag,
|
||||
"--repo",
|
||||
- "fabro-sh/fabro",
|
||||
+ GITHUB_REPO,
|
||||
"--pattern",
|
||||
asset,
|
||||
"--dir",
|
||||
@@ -101,10 +110,9 @@ impl Backend {
|
||||
bail!("gh release download failed with exit code {status}");
|
||||
}
|
||||
}
|
||||
- Backend::Http => {
|
||||
+ Backend::Http(client) => {
|
||||
let url =
|
||||
- format!("https://github.com/fabro-sh/fabro/releases/download/{tag}/{asset}");
|
||||
- let client = reqwest::Client::builder().user_agent("fabro-cli").build()?;
|
||||
+ format!("https://github.com/{GITHUB_REPO}/releases/download/{tag}/{asset}");
|
||||
let resp = client
|
||||
.get(&url)
|
||||
.send()
|
||||
@@ -122,22 +130,26 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
-fn select_backend() -> Backend {
|
||||
+async fn select_backend() -> Backend {
|
||||
// Check if gh is available
|
||||
- let gh_version = std::process::Command::new("gh").arg("--version").output();
|
||||
+ let gh_version = tokio::process::Command::new("gh")
|
||||
+ .arg("--version")
|
||||
+ .output()
|
||||
+ .await;
|
||||
let Ok(output) = gh_version else {
|
||||
debug!("gh CLI not found, using HTTP backend");
|
||||
- return Backend::Http;
|
||||
+ return Backend::Http(http_client().expect("failed to build HTTP client"));
|
||||
};
|
||||
if !output.status.success() {
|
||||
debug!("gh --version failed, using HTTP backend");
|
||||
- return Backend::Http;
|
||||
+ return Backend::Http(http_client().expect("failed to build HTTP client"));
|
||||
}
|
||||
|
||||
// Check if gh is authenticated
|
||||
- let auth_status = std::process::Command::new("gh")
|
||||
+ let auth_status = tokio::process::Command::new("gh")
|
||||
.args(["auth", "status"])
|
||||
- .output();
|
||||
+ .output()
|
||||
+ .await;
|
||||
match auth_status {
|
||||
Ok(o) if o.status.success() => {
|
||||
debug!("gh CLI available and authenticated, using Gh backend");
|
||||
@@ -145,7 +157,7 @@ fn select_backend() -> Backend {
|
||||
}
|
||||
_ => {
|
||||
debug!("gh not authenticated, using HTTP backend");
|
||||
- Backend::Http
|
||||
+ Backend::Http(http_client().expect("failed to build HTTP client"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,30 +179,14 @@ fn parse_version_from_tag(tag: &str) -> Result<Version> {
|
||||
Version::parse(stripped).with_context(|| format!("invalid version: {tag}"))
|
||||
}
|
||||
|
||||
-#[derive(Debug, PartialEq)]
|
||||
-enum VersionComparison {
|
||||
- /// A newer version is available
|
||||
- Newer,
|
||||
- /// Already on the target version
|
||||
- AlreadyCurrent,
|
||||
- /// Target is older than current (downgrade)
|
||||
- Downgrade,
|
||||
-}
|
||||
-
|
||||
-fn compare_versions(current: &Version, target: &Version) -> VersionComparison {
|
||||
- use std::cmp::Ordering;
|
||||
- match target.cmp(current) {
|
||||
- Ordering::Greater => VersionComparison::Newer,
|
||||
- Ordering::Equal => VersionComparison::AlreadyCurrent,
|
||||
- Ordering::Less => VersionComparison::Downgrade,
|
||||
- }
|
||||
-}
|
||||
-
|
||||
// ── SHA256 verification ────────────────────────────────────────────────────
|
||||
|
||||
-fn verify_checksum(data: &[u8], expected_hex: &str) -> Result<()> {
|
||||
+fn verify_checksum(path: &Path, expected_hex: &str) -> Result<()> {
|
||||
let mut hasher = Sha256::new();
|
||||
- hasher.update(data);
|
||||
+ let mut file = std::io::BufReader::new(
|
||||
+ fs::File::open(path).with_context(|| format!("failed to open {}", path.display()))?,
|
||||
+ );
|
||||
+ std::io::copy(&mut file, &mut hasher)?;
|
||||
let computed = format!("{:x}", hasher.finalize());
|
||||
// The .sha256 file may contain "hash filename" or just "hash"
|
||||
let expected = expected_hex
|
||||
@@ -242,7 +238,7 @@ impl UpgradeCheckState {
|
||||
// ── Main upgrade command ───────────────────────────────────────────────────
|
||||
|
||||
pub async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
|
||||
- let backend = select_backend();
|
||||
+ let backend = select_backend().await;
|
||||
|
||||
let current =
|
||||
Version::parse(env!("CARGO_PKG_VERSION")).context("failed to parse current version")?;
|
||||
@@ -259,8 +255,8 @@ pub async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
|
||||
};
|
||||
|
||||
// Downgrade protection
|
||||
- match compare_versions(¤t, &target) {
|
||||
- VersionComparison::Downgrade => {
|
||||
+ match target.cmp(¤t) {
|
||||
+ std::cmp::Ordering::Less => {
|
||||
if args.version.is_none() {
|
||||
bail!(
|
||||
"latest release ({target}) is older than installed version ({current}), skipping"
|
||||
@@ -280,7 +276,7 @@ pub async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
|
||||
bail!("downgrade requires interactive confirmation (stdin is not a tty)");
|
||||
}
|
||||
}
|
||||
- VersionComparison::AlreadyCurrent if !args.force => {
|
||||
+ std::cmp::Ordering::Equal if !args.force => {
|
||||
eprintln!("Already on version {current}");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -307,19 +303,16 @@ pub async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
|
||||
.or_else(|_| tempfile::tempdir())
|
||||
.context("failed to create temp directory")?;
|
||||
|
||||
- // Download tarball and checksum
|
||||
+ // Download tarball and checksum in parallel
|
||||
eprintln!("Downloading fabro {target}...");
|
||||
- let tarball_path = backend
|
||||
- .download_release(&tag, &tarball_name, tmp_dir.path())
|
||||
- .await?;
|
||||
- let checksum_path = backend
|
||||
- .download_release(&tag, &checksum_name, tmp_dir.path())
|
||||
- .await?;
|
||||
-
|
||||
- // Verify SHA256
|
||||
- let tarball_data = fs::read(&tarball_path)?;
|
||||
+ let (tarball_path, checksum_path) = tokio::try_join!(
|
||||
+ backend.download_release(&tag, &tarball_name, tmp_dir.path()),
|
||||
+ backend.download_release(&tag, &checksum_name, tmp_dir.path()),
|
||||
+ )?;
|
||||
+
|
||||
+ // Verify SHA256 using streaming hash
|
||||
let checksum_content = fs::read_to_string(&checksum_path)?;
|
||||
- verify_checksum(&tarball_data, &checksum_content)?;
|
||||
+ verify_checksum(&tarball_path, &checksum_content)?;
|
||||
debug!("SHA256 checksum verified");
|
||||
|
||||
// Extract tarball
|
||||
@@ -336,13 +329,8 @@ pub async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
|
||||
bail!("tar extraction failed");
|
||||
}
|
||||
|
||||
- // Find extracted binary
|
||||
- let extracted_binary = tmp_dir.path().join("fabro");
|
||||
- if !extracted_binary.exists() {
|
||||
- bail!("extracted archive does not contain 'fabro' binary");
|
||||
- }
|
||||
-
|
||||
// Atomic binary replacement
|
||||
+ let extracted_binary = tmp_dir.path().join("fabro");
|
||||
let backup = exe_dir.join(".fabro-upgrade-backup");
|
||||
fs::rename(¤t_exe, &backup).context("failed to move current binary to backup")?;
|
||||
if let Err(e) = fs::rename(&extracted_binary, ¤t_exe) {
|
||||
@@ -369,22 +357,24 @@ pub async fn run_upgrade(args: UpgradeArgs) -> Result<()> {
|
||||
|
||||
// ── Auto version check ────────────────────────────────────────────────────
|
||||
|
||||
-pub async fn maybe_print_upgrade_notice(no_upgrade_check: bool) {
|
||||
- if no_upgrade_check {
|
||||
- return;
|
||||
- }
|
||||
-
|
||||
- if let Err(e) = check_and_print_notice().await {
|
||||
- debug!(%e, "Upgrade check failed (silently swallowed)");
|
||||
- }
|
||||
+/// Spawn a background task that checks for a newer version and prints a notice
|
||||
+/// to stderr after the main command completes. Returns a handle that should be
|
||||
+/// awaited at the end of `main_inner`.
|
||||
+pub fn spawn_upgrade_check(
|
||||
+ no_upgrade_check: bool,
|
||||
+ upgrade_check_enabled: bool,
|
||||
+) -> Option<tokio::task::JoinHandle<()>> {
|
||||
+ if no_upgrade_check || !upgrade_check_enabled {
|
||||
+ return None;
|
||||
+ }
|
||||
+ Some(tokio::spawn(async {
|
||||
+ if let Err(e) = check_and_print_notice().await {
|
||||
+ debug!(%e, "Upgrade check failed (silently swallowed)");
|
||||
+ }
|
||||
+ }))
|
||||
}
|
||||
|
||||
async fn check_and_print_notice() -> Result<()> {
|
||||
- let cli_config = fabro_config::cli::load_cli_config(None)?;
|
||||
- if !cli_config.upgrade_check {
|
||||
- return Ok(());
|
||||
- }
|
||||
-
|
||||
let Some(home) = dirs::home_dir() else {
|
||||
return Ok(());
|
||||
};
|
||||
@@ -405,7 +395,7 @@ async fn check_and_print_notice() -> Result<()> {
|
||||
}
|
||||
|
||||
// Fetch latest version
|
||||
- let backend = select_backend();
|
||||
+ let backend = select_backend().await;
|
||||
let tag = backend.fetch_latest_release_tag().await?;
|
||||
let latest = parse_version_from_tag(&tag)?;
|
||||
|
||||
@@ -471,57 +461,34 @@ mod tests {
|
||||
assert!(parse_version_from_tag("not-a-version").is_err());
|
||||
}
|
||||
|
||||
- // -- Version comparison --
|
||||
-
|
||||
- #[test]
|
||||
- fn compare_newer() {
|
||||
- let current = Version::new(0, 4, 0);
|
||||
- let target = Version::new(0, 5, 0);
|
||||
- assert_eq!(
|
||||
- compare_versions(¤t, &target),
|
||||
- VersionComparison::Newer
|
||||
- );
|
||||
- }
|
||||
-
|
||||
- #[test]
|
||||
- fn compare_already_current() {
|
||||
- let v = Version::new(0, 5, 0);
|
||||
- assert_eq!(compare_versions(&v, &v), VersionComparison::AlreadyCurrent);
|
||||
- }
|
||||
-
|
||||
- #[test]
|
||||
- fn compare_downgrade() {
|
||||
- let current = Version::new(0, 5, 0);
|
||||
- let target = Version::new(0, 4, 0);
|
||||
- assert_eq!(
|
||||
- compare_versions(¤t, &target),
|
||||
- VersionComparison::Downgrade
|
||||
- );
|
||||
- }
|
||||
-
|
||||
// -- SHA256 verification --
|
||||
|
||||
#[test]
|
||||
fn verify_checksum_valid() {
|
||||
- let data = b"hello world";
|
||||
- // sha256 of "hello world"
|
||||
+ let dir = tempfile::tempdir().unwrap();
|
||||
+ let path = dir.path().join("test.bin");
|
||||
+ fs::write(&path, b"hello world").unwrap();
|
||||
let expected = "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9";
|
||||
- assert!(verify_checksum(data, expected).is_ok());
|
||||
+ assert!(verify_checksum(&path, expected).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_checksum_with_filename_suffix() {
|
||||
- let data = b"hello world";
|
||||
+ let dir = tempfile::tempdir().unwrap();
|
||||
+ let path = dir.path().join("test.bin");
|
||||
+ fs::write(&path, b"hello world").unwrap();
|
||||
let expected =
|
||||
"b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9 fabro.tar.gz";
|
||||
- assert!(verify_checksum(data, expected).is_ok());
|
||||
+ assert!(verify_checksum(&path, expected).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_checksum_mismatch() {
|
||||
- let data = b"hello world";
|
||||
+ let dir = tempfile::tempdir().unwrap();
|
||||
+ let path = dir.path().join("test.bin");
|
||||
+ fs::write(&path, b"hello world").unwrap();
|
||||
let wrong = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||
- assert!(verify_checksum(data, wrong).is_err());
|
||||
+ assert!(verify_checksum(&path, wrong).is_err());
|
||||
}
|
||||
|
||||
// -- Upgrade check state --
|
||||
@@ -576,9 +543,9 @@ mod tests {
|
||||
|
||||
// -- Backend selection --
|
||||
|
||||
- #[test]
|
||||
- fn select_backend_returns_a_variant() {
|
||||
+ #[tokio::test]
|
||||
+ async fn select_backend_returns_a_variant() {
|
||||
// Just ensure it doesn't panic; actual variant depends on environment
|
||||
- let _backend = select_backend();
|
||||
+ let _backend = select_backend().await;
|
||||
}
|
||||
}
|
||||
5
nodes/verify/script_invocation.json
Normal file
5
nodes/verify/script_invocation.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"command": "cargo clippy -- -D warnings 2>&1 && cargo test 2>&1",
|
||||
"language": "shell",
|
||||
"timeout_ms": null
|
||||
}
|
||||
5
nodes/verify/script_timing.json
Normal file
5
nodes/verify/script_timing.json
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"duration_ms": 5003,
|
||||
"exit_code": 0,
|
||||
"timed_out": false
|
||||
}
|
||||
6
nodes/verify/status.json
Normal file
6
nodes/verify/status.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"status": "success",
|
||||
"notes": "Script completed: cargo clippy -- -D warnings 2>&1 && cargo test 2>&1",
|
||||
"failure_reason": null,
|
||||
"timestamp": "2026-03-15T23:12:53.397358+00:00"
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue