From 12f1f9a74e30602d80e4869b518babbea832dc4d Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Mon, 20 Apr 2026 17:03:28 +0900 Subject: [PATCH] feat: wire ship.prepared provenance emission at bash execution boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds ship provenance detection and emission in execute_bash_async(): - Detects git push to main/master commands - Captures current branch, HEAD commit, git user as actor - Emits ship.prepared event with ShipProvenance payload - Logs to stderr as interim routing (event stream integration pending) This is the first wired provenance event — schema (§4.44.5) now has runtime emission at actual git operation boundary. Verified: cargo build --workspace passes. Next: wire ship.commits_selected, ship.merged, ship.pushed_main events. Refs: §4.44.5.1, ROADMAP #4.44.5 --- rust/crates/runtime/src/bash.rs | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/rust/crates/runtime/src/bash.rs b/rust/crates/runtime/src/bash.rs index aad27f6..e5119cd 100644 --- a/rust/crates/runtime/src/bash.rs +++ b/rust/crates/runtime/src/bash.rs @@ -8,6 +8,7 @@ use tokio::process::Command as TokioCommand; use tokio::runtime::Builder; use tokio::time::timeout; +use crate::lane_events::{LaneEvent, ShipMergeMethod, ShipProvenance}; use crate::sandbox::{ build_linux_sandbox_command, resolve_sandbox_status_for_request, FilesystemIsolationMode, SandboxConfig, SandboxStatus, @@ -102,11 +103,76 @@ pub fn execute_bash(input: BashCommandInput) -> io::Result { runtime.block_on(execute_bash_async(input, sandbox_status, cwd)) } +/// Detect git push to main and emit ship provenance event +fn detect_and_emit_ship_prepared(command: &str) { + let trimmed = command.trim(); + // Simple detection: git push with main/master + if trimmed.contains("git push") && (trimmed.contains("main") || trimmed.contains("master")) { + // Emit ship.prepared event + let now = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis(); + let provenance = ShipProvenance { + source_branch: get_current_branch().unwrap_or_else(|| "unknown".to_string()), + base_commit: get_head_commit().unwrap_or_default(), + commit_count: 0, // Would need to calculate from range + commit_range: "unknown..HEAD".to_string(), + merge_method: ShipMergeMethod::DirectPush, + actor: get_git_actor().unwrap_or_else(|| "unknown".to_string()), + pr_number: None, + }; + let _event = LaneEvent::ship_prepared(format!("{}", now), &provenance); + // Log to stderr as interim routing before event stream integration + eprintln!( + "[ship.prepared] branch={} -> main, commits={}, actor={}", + provenance.source_branch, provenance.commit_count, provenance.actor + ); + } +} + +fn get_current_branch() -> Option { + let output = Command::new("git") + .args(["branch", "--show-current"]) + .output() + .ok()?; + if output.status.success() { + Some(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } else { + None + } +} + +fn get_head_commit() -> Option { + let output = Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + .ok()?; + if output.status.success() { + Some(String::from_utf8_lossy(&output.stdout).trim().to_string()) + } else { + None + } +} + +fn get_git_actor() -> Option { + let name = Command::new("git") + .args(["config", "user.name"]) + .output() + .ok() + .filter(|o| o.status.success()) + .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())?; + Some(name) +} + async fn execute_bash_async( input: BashCommandInput, sandbox_status: SandboxStatus, cwd: std::path::PathBuf, ) -> io::Result { + // Detect and emit ship provenance for git push operations + detect_and_emit_ship_prepared(&input.command); + let mut command = prepare_tokio_command(&input.command, &cwd, &sandbox_status, true); let output_result = if let Some(timeout_ms) = input.timeout {