refactor(playbook_commands): migrate Chronicle emission to CloudEvents
Replace fake Forgejo push webhook construction with structured CloudEvents 1.0 via ChronicleClient. All playbook governance events now carry structured data fields (playbook name, corpus CID, exit code, duration) instead of unstructured message strings. Event renames: - PLAYBOOK_STARTED -> GOV_PLAYBOOK_STARTED - PLAYBOOK_COMPLETED -> GOV_PLAYBOOK_COMPLETED - ACCORD_LOAD_FAILED -> GOV_ACCORD_LOAD_FAILED - DIFF_MISMATCH_DETECTED -> GOV_DIFF_MISMATCH_DETECTED Signed-off-by: Tyler King <tking@guildhouse.dev>
This commit is contained in:
parent
92464b07c5
commit
869cc610b5
1 changed files with 61 additions and 30 deletions
|
|
@ -3,7 +3,8 @@
|
|||
//! guildhouse-ops playbook run <name> — validates corpus, runs ansible-playbook, Chronicle
|
||||
//! guildhouse-ops playbook list — lists governed playbooks
|
||||
|
||||
use crate::gsap_client::{self, AuthorizeRequest, GsapClient, CompletionReceiptPayload};
|
||||
use crate::chronicle_client::ChronicleClient;
|
||||
use crate::gsap_client::{self, AuthorizeRequest, GsapClient};
|
||||
use crate::session::SessionContext;
|
||||
use crate::traits::OrgCommands;
|
||||
use std::collections::HashMap;
|
||||
|
|
@ -43,21 +44,8 @@ impl PlaybookCommands {
|
|||
if path.is_empty() { None } else { Some(path) }
|
||||
}
|
||||
|
||||
fn emit_chronicle(&self, kind: &str, actor_did: &str, message: &str) -> bool {
|
||||
let body = serde_json::json!({
|
||||
"pusher": {"login": actor_did},
|
||||
"ref": format!("refs/playbook/{}", kind),
|
||||
"repository": {"full_name": "platform/ansible-governance"},
|
||||
"commits": [{"message": format!("{}: {}", kind, message)}],
|
||||
});
|
||||
reqwest::blocking::Client::new()
|
||||
.post(&self.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.chronicle_webhook)
|
||||
}
|
||||
|
||||
fn cmd_run(
|
||||
|
|
@ -103,10 +91,18 @@ impl PlaybookCommands {
|
|||
let actor_did = format!("did:web:{}/user/operator", ctx.trust_domain);
|
||||
let pb_cid = entry.get("spec").and_then(|s| s.get("cid")).and_then(|v| v.as_str()).unwrap_or("?");
|
||||
|
||||
self.emit_chronicle(
|
||||
"PLAYBOOK_STARTED",
|
||||
let chronicle = self.chronicle();
|
||||
chronicle.emit(
|
||||
"GOV_PLAYBOOK_STARTED",
|
||||
&actor_did,
|
||||
&format!("{} cid={} target={}", name, pb_cid, target.unwrap_or("all")),
|
||||
&ChronicleClient::generate_id(),
|
||||
serde_json::json!({
|
||||
"kind": "GOV_PLAYBOOK_STARTED",
|
||||
"description": format!("{} cid={} target={}", name, pb_cid, target.unwrap_or("all")),
|
||||
"playbook": name,
|
||||
"corpus_entry_cid": pb_cid,
|
||||
"target": target.unwrap_or("all"),
|
||||
}),
|
||||
);
|
||||
|
||||
// Helper to build ansible-playbook command
|
||||
|
|
@ -132,10 +128,17 @@ impl PlaybookCommands {
|
|||
Ok(policy) => policy,
|
||||
Err(e) => {
|
||||
eprintln!("\n BLOCKED: Accord load failed: {}", e);
|
||||
self.emit_chronicle(
|
||||
"ACCORD_LOAD_FAILED",
|
||||
chronicle.emit(
|
||||
"GOV_ACCORD_LOAD_FAILED",
|
||||
&actor_did,
|
||||
&format!("{} accord={} error={}", name, accord_name, e),
|
||||
&ChronicleClient::generate_id(),
|
||||
serde_json::json!({
|
||||
"kind": "GOV_ACCORD_LOAD_FAILED",
|
||||
"description": format!("{} accord={} error={}", name, accord_name, e),
|
||||
"playbook": name,
|
||||
"accord": accord_name,
|
||||
"error": e.to_string(),
|
||||
}),
|
||||
);
|
||||
anyhow::bail!(
|
||||
"Accord '{}' could not be loaded. Governed operation blocked. {}",
|
||||
|
|
@ -198,7 +201,17 @@ impl PlaybookCommands {
|
|||
if dry_run {
|
||||
print!("{}", diff);
|
||||
println!("\n [--check] Dry run complete.");
|
||||
self.emit_chronicle("PLAYBOOK_COMPLETED", &actor_did, &format!("{} dry_run=true", name));
|
||||
chronicle.emit(
|
||||
"GOV_PLAYBOOK_COMPLETED",
|
||||
&actor_did,
|
||||
&ChronicleClient::generate_id(),
|
||||
serde_json::json!({
|
||||
"kind": "GOV_PLAYBOOK_COMPLETED",
|
||||
"description": format!("{} dry_run=true", name),
|
||||
"playbook": name,
|
||||
"dry_run": true,
|
||||
}),
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
|
@ -215,8 +228,18 @@ impl PlaybookCommands {
|
|||
let recheck_hash = crate::apply_gate::hash_diff(&recheck_diff);
|
||||
if recheck_hash != diff_hash {
|
||||
eprintln!("\n BLOCKED: Diff changed since MFA sign-off!");
|
||||
self.emit_chronicle("DIFF_MISMATCH_DETECTED", &actor_did,
|
||||
&format!("{} signed={} actual={}", name, &diff_hash[..24], &recheck_hash[..24]));
|
||||
chronicle.emit(
|
||||
"GOV_DIFF_MISMATCH_DETECTED",
|
||||
&actor_did,
|
||||
&ChronicleClient::generate_id(),
|
||||
serde_json::json!({
|
||||
"kind": "GOV_DIFF_MISMATCH_DETECTED",
|
||||
"description": format!("{} signed={} actual={}", name, &diff_hash[..24], &recheck_hash[..24]),
|
||||
"playbook": name,
|
||||
"signed_hash": &diff_hash[..24],
|
||||
"actual_hash": &recheck_hash[..24],
|
||||
}),
|
||||
);
|
||||
anyhow::bail!("Apply blocked: diff changed after MFA.");
|
||||
}
|
||||
} else if !diff.trim().is_empty() {
|
||||
|
|
@ -230,11 +253,19 @@ impl PlaybookCommands {
|
|||
let duration = start.elapsed();
|
||||
let rc = status.code().unwrap_or(-1);
|
||||
|
||||
// Chronicle: PLAYBOOK_COMPLETED
|
||||
self.emit_chronicle(
|
||||
"PLAYBOOK_COMPLETED",
|
||||
// Chronicle: GOV_PLAYBOOK_COMPLETED
|
||||
chronicle.emit(
|
||||
"GOV_PLAYBOOK_COMPLETED",
|
||||
&actor_did,
|
||||
&format!("{} rc={} duration={}s", name, rc, duration.as_secs()),
|
||||
&ChronicleClient::generate_id(),
|
||||
serde_json::json!({
|
||||
"kind": "GOV_PLAYBOOK_COMPLETED",
|
||||
"description": format!("{} rc={} duration={}s", name, rc, duration.as_secs()),
|
||||
"playbook": name,
|
||||
"exit_code": rc,
|
||||
"duration_secs": duration.as_secs(),
|
||||
"success": status.success(),
|
||||
}),
|
||||
);
|
||||
|
||||
// ── GSAP: Post Completion Receipt ─────────────
|
||||
|
|
@ -257,7 +288,7 @@ impl PlaybookCommands {
|
|||
|
||||
if status.success() {
|
||||
println!("\nPlaybook complete. ({:.1}s)", duration.as_secs_f64());
|
||||
println!("Chronicle: PLAYBOOK_COMPLETED recorded");
|
||||
println!("Chronicle: GOV_PLAYBOOK_COMPLETED recorded");
|
||||
} else {
|
||||
anyhow::bail!("ansible-playbook exited with code {}", rc);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue