refactor(git_commands): migrate Chronicle emission to CloudEvents

Replace fake Forgejo push webhook construction with structured
CloudEvents 1.0 via ChronicleClient. Git commit SHAs are now used
as CloudEvent ids for COMMIT_CREATED and PUSH events, enabling
direct correlation between Chronicle entries and git history.

Event renames:
- REPO_CLONED -> GOV_REPO_CLONED
- COMMIT_CREATED -> GOV_COMMIT_CREATED
- GOVERNED_PUSH -> GOV_PUSH
- PR_CREATED -> GOV_PR_CREATED

Signed-off-by: Tyler King <tking@guildhouse.dev>
This commit is contained in:
Tyler J King 2026-04-12 06:51:42 -04:00
parent cf744dd909
commit 92464b07c5

View file

@ -3,6 +3,7 @@
//! Wraps git operations with accord validation, corpus score checks,
//! and Chronicle attribution.
use crate::chronicle_client::ChronicleClient;
use crate::session::SessionContext;
use crate::traits::OrgCommands;
use std::process::Command;
@ -84,21 +85,8 @@ impl GovernedGitCommands {
.map(|s| s.to_string())
}
fn emit_chronicle(&self, kind: &str, actor_did: &str, message: &str) -> bool {
let body = serde_json::json!({
"pusher": {"login": actor_did},
"ref": format!("refs/governed/{}", kind),
"repository": {"full_name": "platform/git-governance"},
"commits": [{"message": format!("{}: {}", kind, message)}],
});
reqwest::blocking::Client::new()
.post(&self.config.chronicle_webhook)
.header("X-Forgejo-Event", "push")
.json(&body)
.timeout(std::time::Duration::from_secs(5))
.send()
.map(|r| r.status().is_success())
.unwrap_or(false)
fn chronicle(&self) -> ChronicleClient {
ChronicleClient::from_legacy_webhook(&self.config.chronicle_webhook)
}
fn cmd_status(&self, _ctx: &SessionContext) -> anyhow::Result<()> {
@ -149,12 +137,18 @@ impl GovernedGitCommands {
if !out.is_empty() {
println!("{}", out.trim());
}
self.emit_chronicle(
"REPO_CLONED",
&format!("did:web:{}/user/operator", ctx.trust_domain),
&format!("repo={}", repo),
let actor_did = format!("did:web:{}/user/operator", ctx.trust_domain);
self.chronicle().emit(
"GOV_REPO_CLONED",
&actor_did,
&ChronicleClient::generate_id(),
serde_json::json!({
"kind": "GOV_REPO_CLONED",
"description": format!("repo={}", repo),
"repo": repo,
}),
);
println!("Cloned. Chronicle: REPO_CLONED");
println!("Cloned. Chronicle: GOV_REPO_CLONED");
Ok(())
}
@ -226,27 +220,45 @@ impl GovernedGitCommands {
println!(" Actor: did:web:{}/user/operator", ctx.trust_domain);
println!("--");
// Emit COMMIT_CREATED for each commit in the push range (0x1704)
// Emit GOV_COMMIT_CREATED for each commit in the push range
let actor_did = format!("did:web:{}/user/operator", ctx.trust_domain);
let chronicle = self.chronicle();
let (commit_log, _, _) =
Self::git(&["log", &format!("{}/{}..HEAD", remote, branch), "--format=%H|%s", "--no-merges"]);
for line in commit_log.lines().filter(|l| !l.is_empty()) {
let parts: Vec<&str> = line.splitn(2, '|').collect();
if parts.len() >= 2 {
self.emit_chronicle(
"COMMIT_CREATED",
let commit_sha = parts[0].trim();
chronicle.emit(
"GOV_COMMIT_CREATED",
&actor_did,
&format!("sha={} msg={}", parts[0], parts[1]),
commit_sha,
serde_json::json!({
"kind": "GOV_COMMIT_CREATED",
"description": format!("sha={} msg={}", commit_sha, parts[1]),
"git_commit": commit_sha,
"message": parts[1],
"git_ref": format!("refs/heads/{}", branch),
}),
);
}
}
// Chronicle event
// Chronicle: GOV_PUSH
let (sha, _, _) = Self::git(&["rev-parse", "HEAD"]);
self.emit_chronicle(
"GOVERNED_PUSH",
let head_sha = sha.trim();
chronicle.emit(
"GOV_PUSH",
&actor_did,
&format!("{}@{} -> {}/{}", sha.trim(), branch, remote, branch),
head_sha,
serde_json::json!({
"kind": "GOV_PUSH",
"description": format!("{}@{} -> {}/{}", head_sha, branch, remote, branch),
"git_commit": head_sha,
"git_ref": format!("refs/heads/{}", branch),
"remote": remote,
"branch": branch,
}),
);
// Actual push
@ -257,7 +269,7 @@ impl GovernedGitCommands {
if !out.is_empty() {
println!("{}", out.trim());
}
println!("Chronicle: GOVERNED_PUSH recorded");
println!("Chronicle: GOV_PUSH recorded");
Ok(())
}
@ -296,10 +308,20 @@ impl GovernedGitCommands {
let data: serde_json::Value = resp.json().unwrap_or_default();
let pr_url = data["html_url"].as_str().unwrap_or("?");
println!("PR created: {}", pr_url);
self.emit_chronicle(
"PR_CREATED",
&format!("did:web:{}/user/operator", ctx.trust_domain),
&format!("PR: {} ({})", title, pr_url),
let actor_did = format!("did:web:{}/user/operator", ctx.trust_domain);
let (sha, _, _) = Self::git(&["rev-parse", "HEAD"]);
self.chronicle().emit(
"GOV_PR_CREATED",
&actor_did,
sha.trim(),
serde_json::json!({
"kind": "GOV_PR_CREATED",
"description": format!("PR: {} ({})", title, pr_url),
"git_commit": sha.trim(),
"branch": branch,
"pr_url": pr_url,
"title": title,
}),
);
}
Ok(resp) => eprintln!("PR creation failed: {}", resp.status()),