feat(chronicle_client): emit GovernanceEnvelope in git events
Add emit_git_event() method to ChronicleClient that embeds a GovernanceEnvelope in the CloudEvent data payload. The CloudEvent id is set to the hex-encoded git_ref from the envelope. Migrate GOV_COMMIT_CREATED, GOV_PUSH, and GOV_PR_CREATED in git_commands.rs to use emit_git_event(). Non-git events continue to use the existing emit() method. Signed-off-by: Tyler King <tking@guildhouse.dev>
This commit is contained in:
parent
4ce225654d
commit
9a4eb378bf
2 changed files with 148 additions and 17 deletions
|
|
@ -9,6 +9,8 @@
|
|||
|
||||
use std::time::Duration;
|
||||
|
||||
use governance_types::GovernanceEnvelope;
|
||||
|
||||
/// CloudEvents type prefix for Guildhouse Chronicle events.
|
||||
const CE_TYPE_PREFIX: &str = "dev.guildhouse.chronicle.";
|
||||
|
||||
|
|
@ -92,6 +94,47 @@ impl ChronicleClient {
|
|||
pub fn generate_id() -> String {
|
||||
uuid::Uuid::new_v4().to_string()
|
||||
}
|
||||
|
||||
/// Emit a git-originated governance event with a full envelope.
|
||||
///
|
||||
/// The CloudEvent `id` is set to the hex-encoded `git_ref` from the
|
||||
/// envelope, so Chronicle's entry_id matches the git SHA.
|
||||
pub fn emit_git_event(
|
||||
&self,
|
||||
kind: &str,
|
||||
envelope: &GovernanceEnvelope,
|
||||
extra_data: serde_json::Value,
|
||||
) -> bool {
|
||||
let git_ref_hex = hex::encode(envelope.git_ref);
|
||||
|
||||
// Merge envelope + extra_data into a single data payload
|
||||
let mut data = extra_data;
|
||||
if let serde_json::Value::Object(ref mut map) = data {
|
||||
map.insert("kind".to_string(), serde_json::json!(kind));
|
||||
map.insert(
|
||||
"envelope".to_string(),
|
||||
serde_json::to_value(envelope).unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
let ce = serde_json::json!({
|
||||
"specversion": "1.0",
|
||||
"type": format!("{}{}", CE_TYPE_PREFIX, kind),
|
||||
"source": &envelope.actor_did,
|
||||
"id": &git_ref_hex,
|
||||
"time": now_rfc3339(),
|
||||
"datacontenttype": "application/json",
|
||||
"data": data,
|
||||
});
|
||||
|
||||
self.http
|
||||
.post(&self.endpoint)
|
||||
.header("Content-Type", "application/cloudevents+json; charset=utf-8")
|
||||
.json(&ce)
|
||||
.send()
|
||||
.map(|r| r.status().is_success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
/// RFC 3339 timestamp for CloudEvents `time` field.
|
||||
|
|
@ -224,6 +267,67 @@ mod tests {
|
|||
assert_eq!((y, m, d), (1970, 1, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emit_git_event_payload_structure() {
|
||||
use governance_types::GovernanceEnvelope;
|
||||
|
||||
let sha = hex::decode("95d09f2b10159347eece71399a7e2e907ea3df4f").unwrap();
|
||||
let mut git_ref = [0u8; 20];
|
||||
git_ref.copy_from_slice(&sha);
|
||||
|
||||
let mut envelope = GovernanceEnvelope::for_commit(
|
||||
git_ref,
|
||||
Some("refs/heads/main"),
|
||||
"guildhouse/substrate",
|
||||
[0; 32],
|
||||
"did:web:guildhouse.dev:user:tking",
|
||||
);
|
||||
envelope.timestamp_ns = 1_000_000;
|
||||
|
||||
let extra = serde_json::json!({
|
||||
"description": "test commit",
|
||||
"branch": "main",
|
||||
});
|
||||
|
||||
// Build the CloudEvent payload as emit_git_event would
|
||||
let git_ref_hex = hex::encode(envelope.git_ref);
|
||||
let mut data = extra.clone();
|
||||
if let serde_json::Value::Object(ref mut map) = data {
|
||||
map.insert("kind".to_string(), serde_json::json!("GOV_COMMIT_CREATED"));
|
||||
map.insert(
|
||||
"envelope".to_string(),
|
||||
serde_json::to_value(&envelope).unwrap(),
|
||||
);
|
||||
}
|
||||
|
||||
let ce = serde_json::json!({
|
||||
"specversion": "1.0",
|
||||
"type": format!("{}GOV_COMMIT_CREATED", CE_TYPE_PREFIX),
|
||||
"source": &envelope.actor_did,
|
||||
"id": &git_ref_hex,
|
||||
"time": now_rfc3339(),
|
||||
"datacontenttype": "application/json",
|
||||
"data": data,
|
||||
});
|
||||
|
||||
// CloudEvents 1.0 required fields
|
||||
assert_eq!(ce["specversion"], "1.0");
|
||||
assert!(ce["type"].as_str().unwrap().starts_with("dev.guildhouse.chronicle."));
|
||||
assert!(!ce["source"].as_str().unwrap().is_empty());
|
||||
assert_eq!(ce["id"], "95d09f2b10159347eece71399a7e2e907ea3df4f");
|
||||
|
||||
// Envelope is embedded in data
|
||||
assert!(ce["data"]["envelope"].is_object());
|
||||
assert_eq!(
|
||||
ce["data"]["envelope"]["git_ref"],
|
||||
"95d09f2b10159347eece71399a7e2e907ea3df4f"
|
||||
);
|
||||
assert_eq!(ce["data"]["envelope"]["git_ref_type"], "commit");
|
||||
|
||||
// CloudEvent id matches envelope.git_ref
|
||||
assert_eq!(ce["id"].as_str().unwrap(), ce["data"]["envelope"]["git_ref"].as_str().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_to_ymd_known_date() {
|
||||
// 2026-04-12 = day 20555 since epoch
|
||||
|
|
|
|||
|
|
@ -6,8 +6,19 @@
|
|||
use crate::chronicle_client::ChronicleClient;
|
||||
use crate::session::SessionContext;
|
||||
use crate::traits::OrgCommands;
|
||||
use governance_types::GovernanceEnvelope;
|
||||
use std::process::Command;
|
||||
|
||||
/// Parse a hex SHA string into a [u8; 20] git ref. Returns zeros on parse failure.
|
||||
fn parse_git_sha(hex_str: &str) -> [u8; 20] {
|
||||
let mut sha = [0u8; 20];
|
||||
if let Ok(bytes) = hex::decode(hex_str.trim()) {
|
||||
let len = bytes.len().min(20);
|
||||
sha[..len].copy_from_slice(&bytes[..len]);
|
||||
}
|
||||
sha
|
||||
}
|
||||
|
||||
pub struct GitConfig {
|
||||
pub forgejo_url: String,
|
||||
pub forgejo_token: Option<String>,
|
||||
|
|
@ -225,20 +236,29 @@ impl GovernedGitCommands {
|
|||
let chronicle = self.chronicle();
|
||||
let (commit_log, _, _) =
|
||||
Self::git(&["log", &format!("{}/{}..HEAD", remote, branch), "--format=%H|%s", "--no-merges"]);
|
||||
let ref_name = format!("refs/heads/{}", branch);
|
||||
let (remote_url, _, _) = Self::git(&["remote", "get-url", remote]);
|
||||
let repo_name = remote_url.trim().trim_end_matches(".git")
|
||||
.rsplit('/').take(2).collect::<Vec<_>>().into_iter().rev()
|
||||
.collect::<Vec<_>>().join("/");
|
||||
|
||||
for line in commit_log.lines().filter(|l| !l.is_empty()) {
|
||||
let parts: Vec<&str> = line.splitn(2, '|').collect();
|
||||
if parts.len() >= 2 {
|
||||
let commit_sha = parts[0].trim();
|
||||
chronicle.emit(
|
||||
"GOV_COMMIT_CREATED",
|
||||
let envelope = GovernanceEnvelope::for_commit(
|
||||
parse_git_sha(commit_sha),
|
||||
Some(&ref_name),
|
||||
&repo_name,
|
||||
[0; 32], // accord_hash populated when accord context is available
|
||||
&actor_did,
|
||||
commit_sha,
|
||||
);
|
||||
chronicle.emit_git_event(
|
||||
"GOV_COMMIT_CREATED",
|
||||
&envelope,
|
||||
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),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
|
@ -247,15 +267,18 @@ impl GovernedGitCommands {
|
|||
// Chronicle: GOV_PUSH
|
||||
let (sha, _, _) = Self::git(&["rev-parse", "HEAD"]);
|
||||
let head_sha = sha.trim();
|
||||
chronicle.emit(
|
||||
"GOV_PUSH",
|
||||
let push_envelope = GovernanceEnvelope::for_commit(
|
||||
parse_git_sha(head_sha),
|
||||
Some(&ref_name),
|
||||
&repo_name,
|
||||
[0; 32],
|
||||
&actor_did,
|
||||
head_sha,
|
||||
);
|
||||
chronicle.emit_git_event(
|
||||
"GOV_PUSH",
|
||||
&push_envelope,
|
||||
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,
|
||||
}),
|
||||
|
|
@ -310,14 +333,18 @@ impl GovernedGitCommands {
|
|||
println!("PR created: {}", 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",
|
||||
let envelope = GovernanceEnvelope::for_commit(
|
||||
parse_git_sha(sha.trim()),
|
||||
Some(&format!("refs/heads/{}", branch)),
|
||||
&repo,
|
||||
[0; 32],
|
||||
&actor_did,
|
||||
sha.trim(),
|
||||
);
|
||||
self.chronicle().emit_git_event(
|
||||
"GOV_PR_CREATED",
|
||||
&envelope,
|
||||
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,
|
||||
|
|
|
|||
Loading…
Reference in a new issue