fix: expose complete version provenance

Generated with https://github.com/Yeachan-Heo/gajae-code

Co-authored-by: Gajae Code <dev@gajae-code.com>
This commit is contained in:
bellman
2026-06-04 15:55:08 +09:00
parent 7dd17c6344
commit ae7da0ec74
7 changed files with 179 additions and 39 deletions

View File

@@ -1,10 +1,9 @@
use std::env;
use std::process::Command;
fn main() {
// Get git SHA (short hash)
let git_sha = Command::new("git")
.args(["rev-parse", "--short", "HEAD"])
fn command_output(program: &str, args: &[&str]) -> Option<String> {
Command::new(program)
.args(args)
.output()
.ok()
.and_then(|output| {
@@ -14,11 +13,37 @@ fn main() {
None
}
})
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string());
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
fn main() {
let git_sha =
command_output("git", &["rev-parse", "HEAD"]).unwrap_or_else(|| "unknown".to_string());
let git_sha_short = command_output("git", &["rev-parse", "--short=12", "HEAD"])
.or_else(|| git_sha.get(..git_sha.len().min(12)).map(str::to_string))
.unwrap_or_else(|| "unknown".to_string());
let git_dirty = command_output("git", &["status", "--porcelain"])
.map(|status| (!status.trim().is_empty()).to_string())
.unwrap_or_else(|| "false".to_string());
let git_branch = command_output("git", &["branch", "--show-current"])
.unwrap_or_else(|| "unknown".to_string());
let git_commit_date = command_output("git", &["show", "-s", "--format=%cI", "HEAD"])
.unwrap_or_else(|| "unknown".to_string());
let git_commit_timestamp = command_output("git", &["show", "-s", "--format=%ct", "HEAD"])
.unwrap_or_else(|| "unknown".to_string());
let rustc_version =
command_output("rustc", &["--version"]).unwrap_or_else(|| "unknown".to_string());
println!("cargo:rustc-env=GIT_SHA={git_sha}");
println!("cargo:rustc-env=GIT_SHA_SHORT={git_sha_short}");
println!("cargo:rustc-env=GIT_DIRTY={git_dirty}");
println!("cargo:rustc-env=GIT_BRANCH={git_branch}");
println!("cargo:rustc-env=GIT_COMMIT_DATE={git_commit_date}");
println!("cargo:rustc-env=GIT_COMMIT_TIMESTAMP={git_commit_timestamp}");
println!("cargo:rustc-env=RUSTC_VERSION={rustc_version}");
// TARGET is always set by Cargo during build
// TARGET is always set by Cargo during build.
let target = env::var("TARGET").unwrap_or_else(|_| "unknown".to_string());
println!("cargo:rustc-env=TARGET={target}");
@@ -35,23 +60,12 @@ fn main() {
})
.or_else(|| std::env::var("BUILD_DATE").ok())
.unwrap_or_else(|| {
// Fall back to current date via `date` command
Command::new("date")
.args(["+%Y-%m-%d"])
.output()
.ok()
.and_then(|o| {
if o.status.success() {
String::from_utf8(o.stdout).ok()
} else {
None
}
})
.map_or_else(|| "unknown".to_string(), |s| s.trim().to_string())
command_output("date", &["+%Y-%m-%d"]).unwrap_or_else(|| "unknown".to_string())
});
println!("cargo:rustc-env=BUILD_DATE={build_date}");
// Rerun if git state changes
println!("cargo:rerun-if-changed=.git/HEAD");
println!("cargo:rerun-if-changed=.git/refs");
// Rerun if git state changes. Paths are relative to this package root.
println!("cargo:rerun-if-changed=../../../.git/HEAD");
println!("cargo:rerun-if-changed=../../../.git/refs");
println!("cargo:rerun-if-changed=../../../.git/index");
}

View File

@@ -268,6 +268,12 @@ const DEFAULT_OAUTH_CALLBACK_PORT: u16 = 4545;
const VERSION: &str = env!("CARGO_PKG_VERSION");
const BUILD_TARGET: Option<&str> = option_env!("TARGET");
const GIT_SHA: Option<&str> = option_env!("GIT_SHA");
const GIT_SHA_SHORT: Option<&str> = option_env!("GIT_SHA_SHORT");
const GIT_DIRTY: Option<&str> = option_env!("GIT_DIRTY");
const GIT_BRANCH: Option<&str> = option_env!("GIT_BRANCH");
const GIT_COMMIT_DATE: Option<&str> = option_env!("GIT_COMMIT_DATE");
const GIT_COMMIT_TIMESTAMP: Option<&str> = option_env!("GIT_COMMIT_TIMESTAMP");
const RUSTC_VERSION: Option<&str> = option_env!("RUSTC_VERSION");
const INTERNAL_PROGRESS_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(3);
const POST_TOOL_STALL_TIMEOUT: Duration = Duration::from_secs(10);
const PRIMARY_SESSION_EXTENSION: &str = "jsonl";
@@ -4452,9 +4458,15 @@ fn version_json_value() -> serde_json::Value {
"kind": "version",
"action": "show",
"status": "ok",
"message": render_version_report(),
"human_readable": render_version_report(),
"version": VERSION,
"git_sha": binary_provenance.git_sha,
"git_sha_short": binary_provenance.git_sha_short,
"is_dirty": binary_provenance.is_dirty,
"branch": binary_provenance.branch,
"commit_date": binary_provenance.commit_date,
"commit_timestamp": binary_provenance.commit_timestamp,
"rustc_version": binary_provenance.rustc_version,
"target": binary_provenance.target,
"build_date": binary_provenance.build_date,
"executable_path": binary_provenance.executable_path,
@@ -4693,6 +4705,12 @@ struct StatusContext {
#[derive(Debug, Clone, PartialEq, Eq)]
struct BinaryProvenance {
git_sha: Option<String>,
git_sha_short: Option<String>,
is_dirty: bool,
branch: Option<String>,
commit_date: String,
commit_timestamp: i64,
rustc_version: String,
target: Option<String>,
build_date: String,
executable_path: Option<String>,
@@ -4714,6 +4732,12 @@ impl BinaryProvenance {
json!({
"status": self.status(),
"git_sha": self.git_sha,
"git_sha_short": self.git_sha_short,
"is_dirty": self.is_dirty,
"branch": self.branch,
"commit_date": self.commit_date,
"commit_timestamp": self.commit_timestamp,
"rustc_version": self.rustc_version,
"target": self.target,
"build_date": self.build_date,
"executable_path": self.executable_path,
@@ -4733,18 +4757,35 @@ fn known_build_metadata(value: Option<&str>) -> Option<String> {
}
}
fn parse_build_bool(value: Option<&str>) -> bool {
value
.map(str::trim)
.is_some_and(|value| value.eq_ignore_ascii_case("true") || value == "1")
}
fn parse_build_timestamp(value: Option<&str>) -> i64 {
value
.and_then(|value| value.trim().parse::<i64>().ok())
.unwrap_or(0)
}
fn binary_provenance_for(cwd: Option<&Path>) -> BinaryProvenance {
let git_sha = known_build_metadata(GIT_SHA);
let git_sha_short = known_build_metadata(GIT_SHA_SHORT).or_else(|| {
git_sha
.as_ref()
.map(|sha| sha.chars().take(12).collect::<String>())
});
let target = known_build_metadata(BUILD_TARGET);
let workspace_git_sha = cwd.and_then(|cwd| {
run_git_capture_in(cwd, &["rev-parse", "--short", "HEAD"])
run_git_capture_in(cwd, &["rev-parse", "HEAD"])
.map(|sha| sha.trim().to_string())
.filter(|sha| !sha.is_empty())
});
let workspace_match = git_sha
.as_deref()
.zip(workspace_git_sha.as_deref())
.map(|(binary, workspace)| binary.starts_with(workspace) || workspace.starts_with(binary));
.map(|(binary, workspace)| binary == workspace);
let hint = if git_sha.is_none() {
Some(
"Build metadata did not include a git SHA; rebuild from a git checkout before filing provenance-sensitive dogfood reports."
@@ -4760,6 +4801,12 @@ fn binary_provenance_for(cwd: Option<&Path>) -> BinaryProvenance {
};
BinaryProvenance {
git_sha,
git_sha_short,
is_dirty: parse_build_bool(GIT_DIRTY),
branch: known_build_metadata(GIT_BRANCH),
commit_date: known_build_metadata(GIT_COMMIT_DATE).unwrap_or_else(|| "unknown".to_string()),
commit_timestamp: parse_build_timestamp(GIT_COMMIT_TIMESTAMP),
rustc_version: known_build_metadata(RUSTC_VERSION).unwrap_or_else(|| "unknown".to_string()),
target,
build_date: DEFAULT_DATE.to_string(),
executable_path: env::current_exe()
@@ -10285,10 +10332,12 @@ fn parse_titled_body(value: &str) -> Option<(String, String)> {
}
fn render_version_report() -> String {
let git_sha = GIT_SHA.unwrap_or("unknown");
let git_sha = GIT_SHA_SHORT.or(GIT_SHA).unwrap_or("unknown");
let target = BUILD_TARGET.unwrap_or("unknown");
let branch = GIT_BRANCH.unwrap_or("unknown");
let dirty = GIT_DIRTY.unwrap_or("unknown");
format!(
"Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Target {target}\n Build date {DEFAULT_DATE}"
"Claw Code\n Version {VERSION}\n Git SHA {git_sha}\n Branch {branch}\n Dirty {dirty}\n Target {target}\n Build date {DEFAULT_DATE}"
)
}

View File

@@ -270,29 +270,88 @@ fn version_emits_json_when_requested() {
"version JSON must have action:show (#711)"
);
assert_eq!(parsed["version"], env!("CARGO_PKG_VERSION"));
// Provenance fields must be present for binary identification (#507).
// Provenance fields must be present for binary identification (#507/#437).
assert!(
parsed.get("message").is_none(),
"version JSON should not duplicate the text report in legacy message; use human_readable instead: {parsed}"
);
assert!(
parsed["human_readable"]
.as_str()
.is_some_and(|text| text.contains("Claw Code")),
"version JSON should keep text output only in human_readable: {parsed}"
);
let git_sha = parsed["git_sha"]
.as_str()
.expect("git_sha must be the full build commit SHA in version JSON");
assert_eq!(git_sha.len(), 40, "git_sha must not be truncated: {parsed}");
assert!(
git_sha.chars().all(|ch| ch.is_ascii_hexdigit()),
"git_sha must be a hex commit id: {parsed}"
);
let git_sha_short = parsed["git_sha_short"]
.as_str()
.expect("version JSON should expose the short SHA as a separate derived field");
assert!(
git_sha.starts_with(git_sha_short),
"git_sha_short should derive from git_sha: {parsed}"
);
assert!(
parsed["is_dirty"].is_boolean(),
"is_dirty should be boolean: {parsed}"
);
assert!(
parsed["branch"].is_string() || parsed["branch"].is_null(),
"branch should be string|null: {parsed}"
);
assert!(
parsed["commit_date"]
.as_str()
.is_some_and(|date| date != "unknown" && date.contains('T')),
"commit_date should be an ISO-8601 commit timestamp string: {parsed}"
);
assert!(
parsed["commit_timestamp"].as_i64().is_some_and(|ts| ts > 0),
"commit_timestamp should be a positive Unix timestamp: {parsed}"
);
assert!(
parsed["rustc_version"]
.as_str()
.is_some_and(|version| version.starts_with("rustc ")),
"rustc_version should identify the compiler: {parsed}"
);
assert!(
parsed["build_date"].is_string(),
"build_date must be a string in version JSON"
);
assert!(
parsed["executable_path"].is_string(),
"executable_path must be a string in version JSON so callers can identify which binary is running"
parsed["executable_path"].as_str().is_some_and(|path| !path.is_empty()),
"executable_path must be a runtime path string so callers can identify which binary is running"
);
let binary_provenance = parsed["binary_provenance"]
.as_object()
.expect("version JSON must include binary_provenance object (#797)");
.expect("version JSON must include binary_provenance object (#797/#437)");
assert!(matches!(
binary_provenance["status"].as_str(),
Some("known" | "unknown")
));
assert_eq!(binary_provenance["git_sha"], parsed["git_sha"]);
assert_eq!(binary_provenance["target"], parsed["target"]);
assert_eq!(binary_provenance["build_date"], parsed["build_date"]);
assert_eq!(
binary_provenance["executable_path"],
parsed["executable_path"]
);
for key in [
"git_sha",
"git_sha_short",
"is_dirty",
"branch",
"commit_date",
"commit_timestamp",
"rustc_version",
"target",
"build_date",
"executable_path",
] {
assert_eq!(
binary_provenance[key], parsed[key],
"binary_provenance.{key} should mirror top-level version field"
);
}
assert!(
binary_provenance["hint"].is_string() || binary_provenance["hint"].is_null(),
"binary provenance must classify missing/stale lineage with a structured hint field"
@@ -334,6 +393,14 @@ fn version_status_doctor_include_binary_provenance_797() {
version["binary_provenance"]["workspace_match"].is_boolean()
|| version["binary_provenance"]["workspace_match"].is_null()
);
let workspace_git_sha = version["binary_provenance"]["workspace_git_sha"]
.as_str()
.expect("workspace git sha should be a string");
assert_eq!(
workspace_git_sha.len(),
40,
"workspace_git_sha should be a full SHA, not a truncated prefix: {version}"
);
let status = assert_json_command(&root, &["--output-format", "json", "status"]);
assert_eq!(status["kind"], "status");
@@ -1518,6 +1585,11 @@ fn resumed_version_and_init_emit_structured_json_when_requested() {
);
assert_eq!(version["kind"], "version");
assert_eq!(version["version"], env!("CARGO_PKG_VERSION"));
assert!(
version.get("message").is_none(),
"resumed /version JSON should not include legacy prose message: {version}"
);
assert!(version["human_readable"].as_str().is_some());
let init = assert_json_command(
&root,

View File

@@ -463,6 +463,9 @@ fn resumed_version_command_emits_structured_json() {
assert!(parsed["version"].as_str().is_some());
assert!(parsed["git_sha"].as_str().is_some());
assert!(parsed["target"].as_str().is_some());
assert!(parsed["git_sha_short"].as_str().is_some());
assert!(parsed.get("message").is_none());
assert!(parsed["human_readable"].as_str().is_some());
}
#[test]