ricecoder_github/managers/
actions_operations.rs1use crate::errors::{GitHubError, Result};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10use super::actions_integration::{
11 CiResultSummary, WorkflowJob, WorkflowStatus,
12};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct WorkflowConfig {
17 pub file_path: String,
19 pub triggers: Vec<String>,
21 pub env_vars: HashMap<String, String>,
23 pub timeout_minutes: u32,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct WorkflowIterationResult {
30 pub original_run_id: u64,
32 pub new_run_id: u64,
34 pub fixes_applied: Vec<String>,
36 pub status: WorkflowStatus,
38 pub timestamp: DateTime<Utc>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct CiResultComment {
45 pub body: String,
47 pub comment_id: Option<u64>,
49 pub created_at: DateTime<Utc>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct WorkflowConfigResult {
56 pub success: bool,
58 pub config: Option<WorkflowConfig>,
60 pub error: Option<String>,
62}
63
64pub struct ActionsOperations;
66
67impl ActionsOperations {
68 pub async fn fix_and_retry(
79 run_id: u64,
80 fixes: Vec<String>,
81 ) -> Result<WorkflowIterationResult> {
82 if run_id == 0 {
83 return Err(GitHubError::invalid_input("Run ID cannot be zero"));
84 }
85 if fixes.is_empty() {
86 return Err(GitHubError::invalid_input("At least one fix must be provided"));
87 }
88
89 Ok(WorkflowIterationResult {
95 original_run_id: run_id,
96 new_run_id: run_id + 1,
97 fixes_applied: fixes,
98 status: WorkflowStatus::Queued,
99 timestamp: Utc::now(),
100 })
101 }
102
103 pub async fn summarize_in_pr_comment(
114 pr_number: u32,
115 summary: CiResultSummary,
116 ) -> Result<CiResultComment> {
117 if pr_number == 0 {
118 return Err(GitHubError::invalid_input("PR number cannot be zero"));
119 }
120
121 let findings = summary
123 .key_findings
124 .iter()
125 .map(|f| format!("- {}", f))
126 .collect::<Vec<_>>()
127 .join("\n");
128 let recommendations = if summary.recommendations.is_empty() {
129 "No recommendations".to_string()
130 } else {
131 summary
132 .recommendations
133 .iter()
134 .map(|r| format!("- {}", r))
135 .collect::<Vec<_>>()
136 .join("\n")
137 };
138 let body = format!(
139 "## CI Results\n\n\
140 **Status**: {:?}\n\n\
141 **Jobs**: {} total, {} passed, {} failed, {} skipped\n\n\
142 **Duration**: {} seconds\n\n\
143 ### Key Findings\n\
144 {}\n\n\
145 ### Recommendations\n\
146 {}",
147 summary.status,
148 summary.total_jobs,
149 summary.passed_jobs,
150 summary.failed_jobs,
151 summary.skipped_jobs,
152 summary.duration_seconds,
153 findings,
154 recommendations
155 );
156
157 Ok(CiResultComment {
158 body,
159 comment_id: None,
160 created_at: Utc::now(),
161 })
162 }
163
164 pub async fn load_workflow_config(config_path: &str) -> Result<WorkflowConfigResult> {
174 if config_path.is_empty() {
175 return Err(GitHubError::invalid_input("Config path cannot be empty"));
176 }
177
178 Ok(WorkflowConfigResult {
184 success: true,
185 config: Some(WorkflowConfig {
186 file_path: config_path.to_string(),
187 triggers: vec!["push".to_string(), "pull_request".to_string()],
188 env_vars: HashMap::new(),
189 timeout_minutes: 60,
190 }),
191 error: None,
192 })
193 }
194
195 pub async fn generate_detailed_report(
207 run_id: u64,
208 status: WorkflowStatus,
209 jobs: Vec<WorkflowJob>,
210 ) -> Result<String> {
211 if run_id == 0 {
212 return Err(GitHubError::invalid_input("Run ID cannot be zero"));
213 }
214
215 let mut report = format!("# Workflow Report\n\nRun ID: {}\n\nStatus: {:?}\n\n", run_id, status);
216
217 report.push_str("## Jobs\n\n");
218 for job in jobs {
219 report.push_str(&format!(
220 "### {}\n- Status: {:?}\n- Conclusion: {}\n\n",
221 job.name,
222 job.status,
223 job.conclusion.as_deref().unwrap_or("N/A")
224 ));
225 }
226
227 Ok(report)
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 #[tokio::test]
236 async fn test_fix_and_retry_with_zero_id() {
237 let result = ActionsOperations::fix_and_retry(0, vec!["fix".to_string()]).await;
238 assert!(result.is_err());
239 }
240
241 #[tokio::test]
242 async fn test_fix_and_retry_with_empty_fixes() {
243 let result = ActionsOperations::fix_and_retry(12345, vec![]).await;
244 assert!(result.is_err());
245 }
246
247 #[tokio::test]
248 async fn test_fix_and_retry_success() {
249 let fixes = vec!["fix1".to_string(), "fix2".to_string()];
250 let result = ActionsOperations::fix_and_retry(12345, fixes.clone()).await;
251 assert!(result.is_ok());
252 let iteration = result.unwrap();
253 assert_eq!(iteration.original_run_id, 12345);
254 assert_eq!(iteration.new_run_id, 12346);
255 assert_eq!(iteration.fixes_applied, fixes);
256 }
257
258 #[tokio::test]
259 async fn test_summarize_in_pr_comment_with_zero_pr() {
260 let summary = CiResultSummary {
261 run_id: 12345,
262 status: WorkflowStatus::Completed,
263 conclusion: Some("success".to_string()),
264 total_jobs: 5,
265 passed_jobs: 5,
266 failed_jobs: 0,
267 skipped_jobs: 0,
268 duration_seconds: 120,
269 key_findings: vec![],
270 recommendations: vec![],
271 };
272 let result = ActionsOperations::summarize_in_pr_comment(0, summary).await;
273 assert!(result.is_err());
274 }
275
276 #[tokio::test]
277 async fn test_summarize_in_pr_comment_success() {
278 let summary = CiResultSummary {
279 run_id: 12345,
280 status: WorkflowStatus::Completed,
281 conclusion: Some("success".to_string()),
282 total_jobs: 5,
283 passed_jobs: 5,
284 failed_jobs: 0,
285 skipped_jobs: 0,
286 duration_seconds: 120,
287 key_findings: vec!["All tests passed".to_string()],
288 recommendations: vec![],
289 };
290 let result = ActionsOperations::summarize_in_pr_comment(42, summary).await;
291 assert!(result.is_ok());
292 let comment = result.unwrap();
293 assert!(comment.body.contains("CI Results"));
294 assert!(comment.body.contains("5 total"));
295 }
296
297 #[tokio::test]
298 async fn test_load_workflow_config_with_empty_path() {
299 let result = ActionsOperations::load_workflow_config("").await;
300 assert!(result.is_err());
301 }
302
303 #[tokio::test]
304 async fn test_load_workflow_config_success() {
305 let result = ActionsOperations::load_workflow_config(".github/workflows/test.yml").await;
306 assert!(result.is_ok());
307 let config_result = result.unwrap();
308 assert!(config_result.success);
309 assert!(config_result.config.is_some());
310 }
311
312 #[tokio::test]
313 async fn test_generate_detailed_report_with_zero_id() {
314 let result = ActionsOperations::generate_detailed_report(0, WorkflowStatus::Completed, vec![])
315 .await;
316 assert!(result.is_err());
317 }
318
319 #[tokio::test]
320 async fn test_generate_detailed_report_success() {
321 let result = ActionsOperations::generate_detailed_report(
322 12345,
323 WorkflowStatus::Completed,
324 vec![],
325 )
326 .await;
327 assert!(result.is_ok());
328 let report = result.unwrap();
329 assert!(report.contains("Workflow Report"));
330 assert!(report.contains("12345"));
331 }
332}