All checks were successful
## Summary - Add HTTP response status checking to all Gitea tracker methods that were silently swallowing errors - Add fallback in create_pr_review: if inline comments fail, retry as plain PR comment ## Test plan - [ ] Deploy and trigger a PR review, check logs for actual error details - [ ] Verify fallback posts summary comment when inline comments fail Co-authored-by: Sharang Parnerkar <parnerkarsharang@gmail.com> Co-authored-by: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com> Reviewed-on: #47
275 lines
8.4 KiB
Rust
275 lines
8.4 KiB
Rust
use compliance_core::error::CoreError;
|
|
use compliance_core::models::{TrackerIssue, TrackerType};
|
|
use compliance_core::traits::issue_tracker::{IssueTracker, ReviewComment};
|
|
use secrecy::{ExposeSecret, SecretString};
|
|
|
|
pub struct GiteaTracker {
|
|
base_url: String,
|
|
http: reqwest::Client,
|
|
token: SecretString,
|
|
}
|
|
|
|
impl GiteaTracker {
|
|
pub fn new(base_url: String, token: SecretString) -> Self {
|
|
Self {
|
|
base_url: base_url.trim_end_matches('/').to_string(),
|
|
http: reqwest::Client::new(),
|
|
token,
|
|
}
|
|
}
|
|
|
|
fn api_url(&self, path: &str) -> String {
|
|
format!("{}/api/v1{}", self.base_url, path)
|
|
}
|
|
}
|
|
|
|
impl IssueTracker for GiteaTracker {
|
|
fn name(&self) -> &str {
|
|
"gitea"
|
|
}
|
|
|
|
async fn create_issue(
|
|
&self,
|
|
owner: &str,
|
|
repo: &str,
|
|
title: &str,
|
|
body: &str,
|
|
labels: &[String],
|
|
) -> Result<TrackerIssue, CoreError> {
|
|
let url = self.api_url(&format!("/repos/{owner}/{repo}/issues"));
|
|
|
|
// Gitea expects label IDs (integers), not names. Append label names
|
|
// to the body instead since resolving IDs would require extra API calls.
|
|
let mut full_body = body.to_string();
|
|
if !labels.is_empty() {
|
|
full_body.push_str("\n\n**Labels:** ");
|
|
full_body.push_str(&labels.join(", "));
|
|
}
|
|
let payload = serde_json::json!({
|
|
"title": title,
|
|
"body": full_body,
|
|
});
|
|
|
|
let resp = self
|
|
.http
|
|
.post(&url)
|
|
.header(
|
|
"Authorization",
|
|
format!("token {}", self.token.expose_secret()),
|
|
)
|
|
.json(&payload)
|
|
.send()
|
|
.await
|
|
.map_err(|e| CoreError::IssueTracker(format!("Gitea create issue failed: {e}")))?;
|
|
|
|
if !resp.status().is_success() {
|
|
let status = resp.status();
|
|
let text = resp.text().await.unwrap_or_default();
|
|
return Err(CoreError::IssueTracker(format!(
|
|
"Gitea returned {status}: {text}"
|
|
)));
|
|
}
|
|
|
|
let issue: serde_json::Value = resp
|
|
.json()
|
|
.await
|
|
.map_err(|e| CoreError::IssueTracker(format!("Failed to parse Gitea response: {e}")))?;
|
|
|
|
Ok(TrackerIssue::new(
|
|
String::new(),
|
|
TrackerType::Gitea,
|
|
issue["number"].to_string(),
|
|
issue["html_url"].as_str().unwrap_or("").to_string(),
|
|
title.to_string(),
|
|
))
|
|
}
|
|
|
|
async fn update_issue_status(
|
|
&self,
|
|
owner: &str,
|
|
repo: &str,
|
|
external_id: &str,
|
|
status: &str,
|
|
) -> Result<(), CoreError> {
|
|
let url = self.api_url(&format!("/repos/{owner}/{repo}/issues/{external_id}"));
|
|
|
|
let state = match status {
|
|
"closed" | "resolved" => "closed",
|
|
_ => "open",
|
|
};
|
|
|
|
let resp = self
|
|
.http
|
|
.patch(&url)
|
|
.header(
|
|
"Authorization",
|
|
format!("token {}", self.token.expose_secret()),
|
|
)
|
|
.json(&serde_json::json!({ "state": state }))
|
|
.send()
|
|
.await
|
|
.map_err(|e| CoreError::IssueTracker(format!("Gitea update issue failed: {e}")))?;
|
|
|
|
if !resp.status().is_success() {
|
|
let status = resp.status();
|
|
let text = resp.text().await.unwrap_or_default();
|
|
return Err(CoreError::IssueTracker(format!(
|
|
"Gitea update issue returned {status}: {text}"
|
|
)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn add_comment(
|
|
&self,
|
|
owner: &str,
|
|
repo: &str,
|
|
external_id: &str,
|
|
body: &str,
|
|
) -> Result<(), CoreError> {
|
|
let url = self.api_url(&format!(
|
|
"/repos/{owner}/{repo}/issues/{external_id}/comments"
|
|
));
|
|
|
|
let resp = self
|
|
.http
|
|
.post(&url)
|
|
.header(
|
|
"Authorization",
|
|
format!("token {}", self.token.expose_secret()),
|
|
)
|
|
.json(&serde_json::json!({ "body": body }))
|
|
.send()
|
|
.await
|
|
.map_err(|e| CoreError::IssueTracker(format!("Gitea add comment failed: {e}")))?;
|
|
|
|
if !resp.status().is_success() {
|
|
let status = resp.status();
|
|
let text = resp.text().await.unwrap_or_default();
|
|
return Err(CoreError::IssueTracker(format!(
|
|
"Gitea add comment returned {status}: {text}"
|
|
)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn create_pr_review(
|
|
&self,
|
|
owner: &str,
|
|
repo: &str,
|
|
pr_number: u64,
|
|
body: &str,
|
|
comments: Vec<ReviewComment>,
|
|
) -> Result<(), CoreError> {
|
|
let url = self.api_url(&format!("/repos/{owner}/{repo}/pulls/{pr_number}/reviews"));
|
|
|
|
let review_comments: Vec<serde_json::Value> = comments
|
|
.iter()
|
|
.map(|c| {
|
|
serde_json::json!({
|
|
"path": c.path,
|
|
"new_position": c.line,
|
|
"body": c.body,
|
|
})
|
|
})
|
|
.collect();
|
|
|
|
let resp = self
|
|
.http
|
|
.post(&url)
|
|
.header(
|
|
"Authorization",
|
|
format!("token {}", self.token.expose_secret()),
|
|
)
|
|
.json(&serde_json::json!({
|
|
"body": body,
|
|
"event": "COMMENT",
|
|
"comments": review_comments,
|
|
}))
|
|
.send()
|
|
.await
|
|
.map_err(|e| CoreError::IssueTracker(format!("Gitea PR review failed: {e}")))?;
|
|
|
|
if !resp.status().is_success() {
|
|
let status = resp.status();
|
|
let text = resp.text().await.unwrap_or_default();
|
|
|
|
// If inline comments caused the failure, retry with just the summary body
|
|
if !comments.is_empty() {
|
|
tracing::warn!(
|
|
"Gitea PR review with inline comments failed ({status}): {text}, retrying as plain comment"
|
|
);
|
|
let fallback_url = self.api_url(&format!(
|
|
"/repos/{owner}/{repo}/issues/{pr_number}/comments"
|
|
));
|
|
let fallback_resp = self
|
|
.http
|
|
.post(&fallback_url)
|
|
.header(
|
|
"Authorization",
|
|
format!("token {}", self.token.expose_secret()),
|
|
)
|
|
.json(&serde_json::json!({ "body": body }))
|
|
.send()
|
|
.await
|
|
.map_err(|e| {
|
|
CoreError::IssueTracker(format!("Gitea PR comment fallback failed: {e}"))
|
|
})?;
|
|
|
|
if !fallback_resp.status().is_success() {
|
|
let fb_status = fallback_resp.status();
|
|
let fb_text = fallback_resp.text().await.unwrap_or_default();
|
|
return Err(CoreError::IssueTracker(format!(
|
|
"Gitea PR comment fallback returned {fb_status}: {fb_text}"
|
|
)));
|
|
}
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
return Err(CoreError::IssueTracker(format!(
|
|
"Gitea PR review returned {status}: {text}"
|
|
)));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
async fn find_existing_issue(
|
|
&self,
|
|
owner: &str,
|
|
repo: &str,
|
|
fingerprint: &str,
|
|
) -> Result<Option<TrackerIssue>, CoreError> {
|
|
let url = self.api_url(&format!(
|
|
"/repos/{owner}/{repo}/issues?type=issues&state=all&q={fingerprint}"
|
|
));
|
|
|
|
let resp = self
|
|
.http
|
|
.get(&url)
|
|
.header(
|
|
"Authorization",
|
|
format!("token {}", self.token.expose_secret()),
|
|
)
|
|
.send()
|
|
.await
|
|
.map_err(|e| CoreError::IssueTracker(format!("Gitea search failed: {e}")))?;
|
|
|
|
let issues: Vec<serde_json::Value> = resp.json().await.unwrap_or_default();
|
|
if let Some(issue) = issues.first() {
|
|
Ok(Some(TrackerIssue::new(
|
|
String::new(),
|
|
TrackerType::Gitea,
|
|
issue["number"].to_string(),
|
|
issue["html_url"].as_str().unwrap_or("").to_string(),
|
|
issue["title"].as_str().unwrap_or("").to_string(),
|
|
)))
|
|
} else {
|
|
Ok(None)
|
|
}
|
|
}
|
|
}
|