ricecoder_github/managers/
actions_operations.rs

1//! GitHub Actions Operations
2//!
3//! Advanced operations for GitHub Actions workflow management and reporting.
4
5use 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/// Workflow configuration
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct WorkflowConfig {
17    /// Workflow file path
18    pub file_path: String,
19    /// Trigger events
20    pub triggers: Vec<String>,
21    /// Environment variables
22    pub env_vars: HashMap<String, String>,
23    /// Timeout in minutes
24    pub timeout_minutes: u32,
25}
26
27/// Workflow iteration result
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct WorkflowIterationResult {
30    /// Original run ID
31    pub original_run_id: u64,
32    /// New run ID after retry
33    pub new_run_id: u64,
34    /// Fixes applied
35    pub fixes_applied: Vec<String>,
36    /// Status after retry
37    pub status: WorkflowStatus,
38    /// Timestamp
39    pub timestamp: DateTime<Utc>,
40}
41
42/// PR comment for CI results
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct CiResultComment {
45    /// Comment body
46    pub body: String,
47    /// Comment ID (if already posted)
48    pub comment_id: Option<u64>,
49    /// Timestamp
50    pub created_at: DateTime<Utc>,
51}
52
53/// Workflow configuration support result
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct WorkflowConfigResult {
56    /// Configuration loaded successfully
57    pub success: bool,
58    /// Configuration details
59    pub config: Option<WorkflowConfig>,
60    /// Error message if failed
61    pub error: Option<String>,
62}
63
64/// GitHub Actions Operations manager
65pub struct ActionsOperations;
66
67impl ActionsOperations {
68    /// Fix issues and re-run workflows
69    ///
70    /// # Arguments
71    ///
72    /// * `run_id` - Original workflow run ID
73    /// * `fixes` - List of fixes to apply
74    ///
75    /// # Returns
76    ///
77    /// Result containing the iteration result
78    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        // In a real implementation, this would:
90        // 1. Apply the fixes to the codebase
91        // 2. Commit the changes
92        // 3. Re-run the workflow
93        // For now, we return a mock result
94        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    /// Summarize CI results in a PR comment
104    ///
105    /// # Arguments
106    ///
107    /// * `pr_number` - PR number
108    /// * `summary` - CI result summary
109    ///
110    /// # Returns
111    ///
112    /// Result containing the PR comment
113    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        // Build the comment body
122        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    /// Support workflow configuration
165    ///
166    /// # Arguments
167    ///
168    /// * `config_path` - Path to workflow configuration file
169    ///
170    /// # Returns
171    ///
172    /// Result containing the workflow configuration
173    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        // In a real implementation, this would:
179        // 1. Read the configuration file
180        // 2. Parse it (YAML or JSON)
181        // 3. Validate the configuration
182        // For now, we return a mock result
183        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    /// Generate detailed workflow report
196    ///
197    /// # Arguments
198    ///
199    /// * `run_id` - Workflow run ID
200    /// * `status` - Workflow status
201    /// * `jobs` - Jobs in the workflow
202    ///
203    /// # Returns
204    ///
205    /// Result containing a detailed report
206    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}