From 685cc18156906b55f264fe498a62f9dbf62bf850 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 9 May 2026 14:10:04 -0400 Subject: [PATCH] fix(build): refresh embedded git sha on branch commits Track the active loose HEAD ref from CLI and server build scripts so local builds refresh FABRO_GIT_SHA after normal branch commits without watching packed-refs. --- Cargo.lock | 6 ++ lib/crates/build-support/Cargo.toml | 13 +++ lib/crates/build-support/git_metadata.rs | 67 ++++++++++++++ lib/crates/fabro-cli/Cargo.toml | 2 + lib/crates/fabro-cli/build.rs | 33 +++---- .../fabro-cli/tests/build_git_metadata.rs | 89 +++++++++++++++++++ lib/crates/fabro-server/Cargo.toml | 1 + lib/crates/fabro-server/build.rs | 33 +++---- 8 files changed, 198 insertions(+), 46 deletions(-) create mode 100644 lib/crates/build-support/Cargo.toml create mode 100644 lib/crates/build-support/git_metadata.rs create mode 100644 lib/crates/fabro-cli/tests/build_git_metadata.rs diff --git a/Cargo.lock b/Cargo.lock index f17264c7a..8e51cbf1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1618,6 +1618,10 @@ dependencies = [ "tokio", ] +[[package]] +name = "fabro-build-support" +version = "0.228.0-nightly.0" + [[package]] name = "fabro-checkpoint" version = "0.228.0-nightly.0" @@ -1657,6 +1661,7 @@ dependencies = [ "fabro-agent", "fabro-api", "fabro-auth", + "fabro-build-support", "fabro-checkpoint", "fabro-client", "fabro-config", @@ -2134,6 +2139,7 @@ dependencies = [ "fabro-agent", "fabro-api", "fabro-auth", + "fabro-build-support", "fabro-config", "fabro-github", "fabro-graphviz", diff --git a/lib/crates/build-support/Cargo.toml b/lib/crates/build-support/Cargo.toml new file mode 100644 index 000000000..410656f74 --- /dev/null +++ b/lib/crates/build-support/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "fabro-build-support" +edition.workspace = true +version.workspace = true +publish = false +license.workspace = true +description = "Build script support utilities for Fabro crates" + +[lib] +path = "git_metadata.rs" + +[lints] +workspace = true diff --git a/lib/crates/build-support/git_metadata.rs b/lib/crates/build-support/git_metadata.rs new file mode 100644 index 000000000..23f427a2d --- /dev/null +++ b/lib/crates/build-support/git_metadata.rs @@ -0,0 +1,67 @@ +use std::path::{Path, PathBuf}; +use std::process::Command; + +#[derive(Debug, Eq, PartialEq)] +pub struct BuildGitMetadata { + pub rerun_paths: Vec, + pub short_sha: String, +} + +pub fn collect_from(package_dir: &Path) -> BuildGitMetadata { + let mut rerun_paths = Vec::new(); + + if let Some(head_path) = git_output(package_dir, ["rev-parse", "--git-path", "HEAD"]) { + rerun_paths.push(PathBuf::from(head_path)); + } + + if let Some(head_ref) = git_output(package_dir, ["symbolic-ref", "-q", "HEAD"]) { + if let Some(ref_path) = git_output(package_dir, ["rev-parse", "--git-path", &head_ref]) { + rerun_paths.push(PathBuf::from(ref_path)); + } + } + + let short_sha = git_output(package_dir, ["rev-list", "-1", "HEAD"]) + .map(|sha| { + if sha.len() >= 7 { + sha[..7].to_string() + } else { + sha + } + }) + .unwrap_or_default(); + + BuildGitMetadata { + rerun_paths, + short_sha, + } +} + +#[expect( + clippy::disallowed_methods, + reason = "Build scripts read Cargo-provided PROFILE outside application runtime configuration." +)] +pub fn cargo_profile() -> String { + std::env::var("PROFILE").unwrap_or_default() +} + +#[expect( + clippy::disallowed_methods, + reason = "Build scripts run outside Tokio and need synchronous git probes for embedded build metadata." +)] +fn git_output(package_dir: &Path, args: [&str; N]) -> Option { + Command::new("git") + .current_dir(package_dir) + .args(args) + .output() + .ok() + .and_then(|output| { + if output.status.success() { + String::from_utf8(output.stdout) + .ok() + .map(|output| output.trim().to_string()) + } else { + None + } + }) + .filter(|output| !output.is_empty()) +} diff --git a/lib/crates/fabro-cli/Cargo.toml b/lib/crates/fabro-cli/Cargo.toml index 866b46a7d..d3f4c94d2 100644 --- a/lib/crates/fabro-cli/Cargo.toml +++ b/lib/crates/fabro-cli/Cargo.toml @@ -108,10 +108,12 @@ core-foundation = { version = "0.9", optional = true } openssl = { version = "0.10", features = ["vendored"] } [build-dependencies] +fabro-build-support = { path = "../build-support" } chrono = { workspace = true } [dev-dependencies] assert_cmd = "2" +fabro-build-support = { path = "../build-support" } fabro-server = { path = "../fabro-server", features = ["test-support"] } insta = { workspace = true, features = ["filters"] } paste = "1" diff --git a/lib/crates/fabro-cli/build.rs b/lib/crates/fabro-cli/build.rs index cfa081207..3079415ee 100644 --- a/lib/crates/fabro-cli/build.rs +++ b/lib/crates/fabro-cli/build.rs @@ -1,31 +1,18 @@ -#[expect( - clippy::disallowed_methods, - reason = "Build scripts run outside Tokio and need a synchronous git probe for the embedded build SHA." -)] -fn main() { - println!("cargo:rerun-if-changed=../../../.git/HEAD"); +use std::path::Path; - let sha = std::process::Command::new("git") - .args(["rev-list", "-1", "HEAD"]) - .output() - .ok() - .and_then(|o| { - if o.status.success() { - String::from_utf8(o.stdout) - .ok() - .map(|s| s.trim().to_string()) - } else { - None - } - }) - .unwrap_or_default(); - let short_sha = if sha.len() >= 7 { &sha[..7] } else { &sha }; - println!("cargo:rustc-env=FABRO_GIT_SHA={short_sha}"); +fn main() { + let metadata = fabro_build_support::collect_from(Path::new(".")); + + for path in metadata.rerun_paths { + println!("cargo:rerun-if-changed={}", path.display()); + } + + println!("cargo:rustc-env=FABRO_GIT_SHA={}", metadata.short_sha); let build_date = chrono::Utc::now().format("%Y-%m-%d").to_string(); println!("cargo:rustc-env=FABRO_BUILD_DATE={build_date}"); - let profile = std::env::var("PROFILE").unwrap_or_default(); + let profile = fabro_build_support::cargo_profile(); let profile_suffix = if profile == "release" { String::new() } else { diff --git a/lib/crates/fabro-cli/tests/build_git_metadata.rs b/lib/crates/fabro-cli/tests/build_git_metadata.rs new file mode 100644 index 000000000..55ab5166a --- /dev/null +++ b/lib/crates/fabro-cli/tests/build_git_metadata.rs @@ -0,0 +1,89 @@ +use std::path::PathBuf; +use std::process::Command; + +#[test] +fn symbolic_branch_tracks_head_and_active_ref() { + let repo = git_repo(); + + let metadata = fabro_build_support::collect_from(repo.path()); + + assert_eq!(metadata.rerun_paths, vec![ + PathBuf::from(".git/HEAD"), + PathBuf::from(".git/refs/heads/main"), + ]); + assert_eq!(metadata.short_sha.len(), 7); +} + +#[test] +fn detached_head_tracks_only_head() { + let repo = git_repo(); + git(repo.path(), ["checkout", "--detach", "HEAD"]); + + let metadata = fabro_build_support::collect_from(repo.path()); + + assert_eq!(metadata.rerun_paths, vec![PathBuf::from(".git/HEAD")]); + assert_eq!(metadata.short_sha.len(), 7); +} + +#[test] +fn non_git_directory_has_no_metadata() { + let dir = tempfile::tempdir().expect("temp dir should create"); + + let metadata = fabro_build_support::collect_from(dir.path()); + + assert!(metadata.rerun_paths.is_empty()); + assert!(metadata.short_sha.is_empty()); +} + +#[test] +fn packed_refs_are_not_tracked() { + let repo = git_repo(); + git(repo.path(), ["pack-refs", "--all", "--prune"]); + + let metadata = fabro_build_support::collect_from(repo.path()); + + assert_eq!(metadata.rerun_paths, vec![ + PathBuf::from(".git/HEAD"), + PathBuf::from(".git/refs/heads/main"), + ]); + assert!( + !metadata + .rerun_paths + .iter() + .any(|path| path.ends_with("packed-refs")) + ); +} + +#[expect( + clippy::disallowed_methods, + reason = "This test creates a small synchronous Git fixture before exercising build metadata collection." +)] +fn git_repo() -> tempfile::TempDir { + let repo = tempfile::tempdir().expect("temp repo should create"); + git(repo.path(), ["init"]); + git(repo.path(), ["symbolic-ref", "HEAD", "refs/heads/main"]); + git(repo.path(), ["config", "user.email", "fabro@example.test"]); + git(repo.path(), ["config", "user.name", "Fabro Test"]); + std::fs::write(repo.path().join("README.md"), "test\n").expect("readme should write"); + git(repo.path(), ["add", "README.md"]); + git(repo.path(), ["commit", "-m", "initial"]); + repo +} + +#[expect( + clippy::disallowed_methods, + reason = "This test fixture uses synchronous Git commands to set up repository states." +)] +fn git(dir: &std::path::Path, args: [&str; N]) { + let output = Command::new("git") + .current_dir(dir) + .args(args) + .output() + .expect("git should run"); + assert!( + output.status.success(), + "git command failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); +} diff --git a/lib/crates/fabro-server/Cargo.toml b/lib/crates/fabro-server/Cargo.toml index 559310a5a..a6dad9a56 100644 --- a/lib/crates/fabro-server/Cargo.toml +++ b/lib/crates/fabro-server/Cargo.toml @@ -94,6 +94,7 @@ zeroize.workspace = true strum.workspace = true [build-dependencies] +fabro-build-support = { path = "../build-support" } chrono = { workspace = true } [dev-dependencies] diff --git a/lib/crates/fabro-server/build.rs b/lib/crates/fabro-server/build.rs index be4cf3e7b..a9337b54e 100644 --- a/lib/crates/fabro-server/build.rs +++ b/lib/crates/fabro-server/build.rs @@ -1,30 +1,17 @@ -#[expect( - clippy::disallowed_methods, - reason = "Build scripts run outside Tokio and need a synchronous git probe for the embedded build SHA." -)] -fn main() { - println!("cargo:rerun-if-changed=../../../.git/HEAD"); +use std::path::Path; - let sha = std::process::Command::new("git") - .args(["rev-list", "-1", "HEAD"]) - .output() - .ok() - .and_then(|o| { - if o.status.success() { - String::from_utf8(o.stdout) - .ok() - .map(|s| s.trim().to_string()) - } else { - None - } - }) - .unwrap_or_default(); - let short_sha = if sha.len() >= 7 { &sha[..7] } else { &sha }; - println!("cargo:rustc-env=FABRO_GIT_SHA={short_sha}"); +fn main() { + let metadata = fabro_build_support::collect_from(Path::new(".")); + + for path in metadata.rerun_paths { + println!("cargo:rerun-if-changed={}", path.display()); + } + + println!("cargo:rustc-env=FABRO_GIT_SHA={}", metadata.short_sha); let build_date = chrono::Utc::now().format("%Y-%m-%d").to_string(); println!("cargo:rustc-env=FABRO_BUILD_DATE={build_date}"); - let profile = std::env::var("PROFILE").unwrap_or_default(); + let profile = fabro_build_support::cargo_profile(); println!("cargo:rustc-env=FABRO_BUILD_PROFILE={profile}"); }