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.
This commit is contained in:
Bryan Helmkamp 2026-05-09 14:10:04 -04:00
parent bfb633ba6e
commit 685cc18156
No known key found for this signature in database
8 changed files with 198 additions and 46 deletions

6
Cargo.lock generated
View file

@ -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",

View file

@ -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

View file

@ -0,0 +1,67 @@
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Eq, PartialEq)]
pub struct BuildGitMetadata {
pub rerun_paths: Vec<PathBuf>,
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<const N: usize>(package_dir: &Path, args: [&str; N]) -> Option<String> {
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())
}

View file

@ -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"

View file

@ -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 {

View file

@ -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<const N: usize>(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)
);
}

View file

@ -94,6 +94,7 @@ zeroize.workspace = true
strum.workspace = true
[build-dependencies]
fabro-build-support = { path = "../build-support" }
chrono = { workspace = true }
[dev-dependencies]

View file

@ -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}");
}