mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-11 22:53:00 +00:00
Save pull request metadata to pull_request.json after PR creation
Return a PullRequestRecord struct from maybe_open_pull_request instead of a bare URL, persist it as JSON in the run/logs directory, and strip markdown heading prefixes (# / ##) from generated PR titles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
56c91b1d66
commit
d1523ea36f
3 changed files with 110 additions and 15 deletions
|
|
@ -89,7 +89,7 @@ async fn pr_create_from(
|
|||
.model
|
||||
.unwrap_or_else(|| arc_llm::catalog::default_model().id.to_string());
|
||||
|
||||
let url = crate::pull_request::maybe_open_pull_request(
|
||||
let record = crate::pull_request::maybe_open_pull_request(
|
||||
&creds,
|
||||
&origin_url,
|
||||
base_branch,
|
||||
|
|
@ -101,10 +101,13 @@ async fn pr_create_from(
|
|||
.await
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?;
|
||||
|
||||
match url {
|
||||
Some(url) => {
|
||||
info!(pr_url = %url, "Pull request created");
|
||||
println!("{url}");
|
||||
match record {
|
||||
Some(record) => {
|
||||
info!(pr_url = %record.html_url, "Pull request created");
|
||||
if let Err(e) = record.save(&run_dir.join("pull_request.json")) {
|
||||
tracing::warn!(error = %e, "Failed to save pull_request.json");
|
||||
}
|
||||
println!("{}", record.html_url);
|
||||
}
|
||||
None => {
|
||||
println!("No pull request created (empty diff).");
|
||||
|
|
|
|||
|
|
@ -1080,8 +1080,15 @@ pub async fn run_command(
|
|||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(url)) => {
|
||||
eprintln!("{} {url}", styles.bold.apply_to("Pull request:"));
|
||||
Ok(Some(record)) => {
|
||||
eprintln!(
|
||||
"{} {}",
|
||||
styles.bold.apply_to("Pull request:"),
|
||||
record.html_url
|
||||
);
|
||||
if let Err(e) = record.save(&logs_dir.join("pull_request.json")) {
|
||||
tracing::warn!(error = %e, "Failed to save pull_request.json");
|
||||
}
|
||||
}
|
||||
Ok(None) => {} // empty diff, logged at DEBUG
|
||||
Err(e) => {
|
||||
|
|
|
|||
|
|
@ -1,12 +1,39 @@
|
|||
use std::path::Path;
|
||||
|
||||
use serde::Serialize;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use arc_github::{self as github_app, ssh_url_to_https, GitHubAppCredentials};
|
||||
|
||||
/// Record of a pull request created for a workflow run.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PullRequestRecord {
|
||||
pub html_url: String,
|
||||
pub number: u64,
|
||||
pub owner: String,
|
||||
pub repo: String,
|
||||
pub base_branch: String,
|
||||
pub head_branch: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl PullRequestRecord {
|
||||
pub fn save(&self, path: &Path) -> Result<(), String> {
|
||||
let json = serde_json::to_string_pretty(self)
|
||||
.map_err(|e| format!("Failed to serialize pull_request.json: {e}"))?;
|
||||
std::fs::write(path, json).map_err(|e| format!("Failed to write pull_request.json: {e}"))
|
||||
}
|
||||
}
|
||||
|
||||
/// Derive a PR title from the workflow goal.
|
||||
///
|
||||
/// Uses the first line, truncated to 120 characters for readability.
|
||||
fn pr_title_from_goal(goal: &str) -> String {
|
||||
let first_line = goal.lines().next().unwrap_or(goal);
|
||||
let first_line = first_line
|
||||
.strip_prefix("## ")
|
||||
.or_else(|| first_line.strip_prefix("# "))
|
||||
.unwrap_or(first_line);
|
||||
if first_line.chars().count() > 120 {
|
||||
let truncated: String = first_line.chars().take(119).collect();
|
||||
format!("{truncated}…")
|
||||
|
|
@ -53,8 +80,8 @@ pub async fn generate_pr_body(diff: &str, goal: &str, model: &str) -> Result<Str
|
|||
|
||||
/// Optionally open a pull request after a successful workflow run.
|
||||
///
|
||||
/// Returns `Ok(Some(html_url))` if a PR was created, `Ok(None)` if the diff
|
||||
/// was empty, or `Err` on failure.
|
||||
/// Returns `Ok(Some(PullRequestRecord))` if a PR was created, `Ok(None)` if
|
||||
/// the diff was empty, or `Err` on failure.
|
||||
pub async fn maybe_open_pull_request(
|
||||
creds: &GitHubAppCredentials,
|
||||
origin_url: &str,
|
||||
|
|
@ -63,7 +90,7 @@ pub async fn maybe_open_pull_request(
|
|||
goal: &str,
|
||||
diff: &str,
|
||||
model: &str,
|
||||
) -> Result<Option<String>, String> {
|
||||
) -> Result<Option<PullRequestRecord>, String> {
|
||||
if diff.is_empty() {
|
||||
debug!("Empty diff, skipping pull request creation");
|
||||
return Ok(None);
|
||||
|
|
@ -77,7 +104,7 @@ pub async fn maybe_open_pull_request(
|
|||
|
||||
let title = pr_title_from_goal(goal);
|
||||
|
||||
let (url, pr_number) = github_app::create_pull_request(
|
||||
let (html_url, number) = github_app::create_pull_request(
|
||||
creds,
|
||||
&owner,
|
||||
&repo,
|
||||
|
|
@ -88,9 +115,17 @@ pub async fn maybe_open_pull_request(
|
|||
)
|
||||
.await?;
|
||||
|
||||
info!(pr_url = %url, pr_number, "Pull request created");
|
||||
info!(pr_url = %html_url, number, "Pull request created");
|
||||
|
||||
Ok(Some(url))
|
||||
Ok(Some(PullRequestRecord {
|
||||
html_url,
|
||||
number,
|
||||
owner,
|
||||
repo,
|
||||
base_branch: base_branch.to_string(),
|
||||
head_branch: head_branch.to_string(),
|
||||
title,
|
||||
}))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -99,8 +134,32 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn pr_title_uses_first_line() {
|
||||
let goal = "# Add Draft PR Mode\n\nMore details here...";
|
||||
assert_eq!(pr_title_from_goal(goal), "# Add Draft PR Mode");
|
||||
let goal = "Add Draft PR Mode\n\nMore details here...";
|
||||
assert_eq!(pr_title_from_goal(goal), "Add Draft PR Mode");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pr_title_strips_h1_prefix() {
|
||||
assert_eq!(
|
||||
pr_title_from_goal("# Add Draft PR Mode"),
|
||||
"Add Draft PR Mode"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pr_title_strips_h2_prefix() {
|
||||
assert_eq!(
|
||||
pr_title_from_goal("## Add Draft PR Mode"),
|
||||
"Add Draft PR Mode"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pr_title_does_not_strip_h3_prefix() {
|
||||
assert_eq!(
|
||||
pr_title_from_goal("### Add Draft PR Mode"),
|
||||
"### Add Draft PR Mode"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -130,6 +189,32 @@ mod tests {
|
|||
assert_eq!(pr_title_from_goal("Fix bug"), "Fix bug");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pull_request_record_save_writes_json() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let path = tmp.path().join("pull_request.json");
|
||||
let record = PullRequestRecord {
|
||||
html_url: "https://github.com/owner/repo/pull/42".to_string(),
|
||||
number: 42,
|
||||
owner: "owner".to_string(),
|
||||
repo: "repo".to_string(),
|
||||
base_branch: "main".to_string(),
|
||||
head_branch: "arc/run/abc".to_string(),
|
||||
title: "Fix the thing".to_string(),
|
||||
};
|
||||
record.save(&path).unwrap();
|
||||
|
||||
let content: serde_json::Value =
|
||||
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
|
||||
assert_eq!(content["html_url"], "https://github.com/owner/repo/pull/42");
|
||||
assert_eq!(content["number"], 42);
|
||||
assert_eq!(content["owner"], "owner");
|
||||
assert_eq!(content["repo"], "repo");
|
||||
assert_eq!(content["base_branch"], "main");
|
||||
assert_eq!(content["head_branch"], "arc/run/abc");
|
||||
assert_eq!(content["title"], "Fix the thing");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_diff_returns_none() {
|
||||
let creds = GitHubAppCredentials {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue