ricecoder_github/managers/
actions_integration.rs

1//! GitHub Actions Integration
2//!
3//! Manages GitHub Actions workflows, including triggering, status tracking, and diagnostics.
4
5use crate::errors::{GitHubError, Result};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Workflow status
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum WorkflowStatus {
14    /// Workflow is queued
15    Queued,
16    /// Workflow is in progress
17    InProgress,
18    /// Workflow completed successfully
19    Completed,
20    /// Workflow failed
21    Failed,
22    /// Workflow was cancelled
23    Cancelled,
24    /// Workflow is skipped
25    Skipped,
26}
27
28/// Workflow run information
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct WorkflowRun {
31    /// Run ID
32    pub id: u64,
33    /// Run number
34    pub run_number: u32,
35    /// Workflow name
36    pub name: String,
37    /// Current status
38    pub status: WorkflowStatus,
39    /// Conclusion (success, failure, cancelled, etc.)
40    pub conclusion: Option<String>,
41    /// Head branch
42    pub head_branch: String,
43    /// Head SHA
44    pub head_sha: String,
45    /// Created at timestamp
46    pub created_at: DateTime<Utc>,
47    /// Updated at timestamp
48    pub updated_at: DateTime<Utc>,
49    /// HTML URL
50    pub html_url: String,
51}
52
53/// Workflow job information
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct WorkflowJob {
56    /// Job ID
57    pub id: u64,
58    /// Job name
59    pub name: String,
60    /// Job status
61    pub status: WorkflowStatus,
62    /// Job conclusion
63    pub conclusion: Option<String>,
64    /// Started at timestamp
65    pub started_at: DateTime<Utc>,
66    /// Completed at timestamp
67    pub completed_at: Option<DateTime<Utc>>,
68    /// Steps in the job
69    pub steps: Vec<JobStep>,
70}
71
72/// Job step information
73#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct JobStep {
75    /// Step name
76    pub name: String,
77    /// Step status
78    pub status: WorkflowStatus,
79    /// Step conclusion
80    pub conclusion: Option<String>,
81    /// Step output
82    pub output: Option<String>,
83}
84
85/// CI failure diagnostic information
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct CiFailureDiagnostics {
88    /// Failed jobs
89    pub failed_jobs: Vec<WorkflowJob>,
90    /// Error logs
91    pub error_logs: Vec<String>,
92    /// Failed steps
93    pub failed_steps: Vec<JobStep>,
94    /// Recommendations for fixing
95    pub recommendations: Vec<String>,
96    /// Timestamp of diagnosis
97    pub diagnosed_at: DateTime<Utc>,
98}
99
100/// Workflow trigger request
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct WorkflowTriggerRequest {
103    /// Workflow file name or ID
104    pub workflow: String,
105    /// Branch to run on
106    pub ref_branch: String,
107    /// Input parameters
108    pub inputs: HashMap<String, String>,
109}
110
111/// Workflow trigger result
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct WorkflowTriggerResult {
114    /// Run ID
115    pub run_id: u64,
116    /// Run number
117    pub run_number: u32,
118    /// Status
119    pub status: WorkflowStatus,
120    /// HTML URL
121    pub html_url: String,
122}
123
124/// Workflow status tracking result
125#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct WorkflowStatusResult {
127    /// Run ID
128    pub run_id: u64,
129    /// Current status
130    pub status: WorkflowStatus,
131    /// Conclusion
132    pub conclusion: Option<String>,
133    /// Progress percentage (0-100)
134    pub progress: u8,
135    /// Jobs in the workflow
136    pub jobs: Vec<WorkflowJob>,
137    /// Last updated
138    pub updated_at: DateTime<Utc>,
139}
140
141/// Workflow retry result
142#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct WorkflowRetryResult {
144    /// New run ID
145    pub new_run_id: u64,
146    /// New run number
147    pub new_run_number: u32,
148    /// Status
149    pub status: WorkflowStatus,
150    /// HTML URL
151    pub html_url: String,
152}
153
154/// CI result summary
155#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct CiResultSummary {
157    /// Run ID
158    pub run_id: u64,
159    /// Overall status
160    pub status: WorkflowStatus,
161    /// Conclusion
162    pub conclusion: Option<String>,
163    /// Total jobs
164    pub total_jobs: u32,
165    /// Passed jobs
166    pub passed_jobs: u32,
167    /// Failed jobs
168    pub failed_jobs: u32,
169    /// Skipped jobs
170    pub skipped_jobs: u32,
171    /// Duration in seconds
172    pub duration_seconds: u64,
173    /// Key findings
174    pub key_findings: Vec<String>,
175    /// Recommendations
176    pub recommendations: Vec<String>,
177}
178
179/// GitHub Actions Integration manager
180pub struct ActionsIntegration {
181    /// GitHub token
182    #[allow(dead_code)]
183    pub token: String,
184    /// Repository owner
185    pub owner: String,
186    /// Repository name
187    pub repo: String,
188}
189
190impl ActionsIntegration {
191    /// Create a new ActionsIntegration manager
192    pub fn new(token: String, owner: String, repo: String) -> Self {
193        Self { token, owner, repo }
194    }
195
196    /// Trigger a GitHub Actions workflow
197    ///
198    /// # Arguments
199    ///
200    /// * `request` - Workflow trigger request
201    ///
202    /// # Returns
203    ///
204    /// Result containing the workflow trigger result
205    pub async fn trigger_workflow(
206        &self,
207        request: WorkflowTriggerRequest,
208    ) -> Result<WorkflowTriggerResult> {
209        // Validate inputs
210        if request.workflow.is_empty() {
211            return Err(GitHubError::invalid_input("Workflow name cannot be empty"));
212        }
213        if request.ref_branch.is_empty() {
214            return Err(GitHubError::invalid_input("Branch name cannot be empty"));
215        }
216
217        // In a real implementation, this would call the GitHub API
218        // For now, we return a mock result
219        Ok(WorkflowTriggerResult {
220            run_id: 12345,
221            run_number: 42,
222            status: WorkflowStatus::Queued,
223            html_url: format!(
224                "https://github.com/{}/{}/actions/runs/12345",
225                self.owner, self.repo
226            ),
227        })
228    }
229
230    /// Track workflow status and report results
231    ///
232    /// # Arguments
233    ///
234    /// * `run_id` - Workflow run ID
235    ///
236    /// # Returns
237    ///
238    /// Result containing the workflow status
239    pub async fn track_workflow_status(&self, run_id: u64) -> Result<WorkflowStatusResult> {
240        if run_id == 0 {
241            return Err(GitHubError::invalid_input("Run ID cannot be zero"));
242        }
243
244        // In a real implementation, this would query the GitHub API
245        // For now, we return a mock result
246        Ok(WorkflowStatusResult {
247            run_id,
248            status: WorkflowStatus::InProgress,
249            conclusion: None,
250            progress: 50,
251            jobs: vec![],
252            updated_at: Utc::now(),
253        })
254    }
255
256    /// Respond to CI failures with diagnostic information
257    ///
258    /// # Arguments
259    ///
260    /// * `run_id` - Workflow run ID
261    ///
262    /// # Returns
263    ///
264    /// Result containing CI failure diagnostics
265    pub async fn diagnose_ci_failure(&self, run_id: u64) -> Result<CiFailureDiagnostics> {
266        if run_id == 0 {
267            return Err(GitHubError::invalid_input("Run ID cannot be zero"));
268        }
269
270        // In a real implementation, this would fetch job logs and analyze failures
271        // For now, we return a mock result
272        Ok(CiFailureDiagnostics {
273            failed_jobs: vec![],
274            error_logs: vec![],
275            failed_steps: vec![],
276            recommendations: vec![
277                "Check the error logs for more details".to_string(),
278                "Verify all dependencies are installed".to_string(),
279            ],
280            diagnosed_at: Utc::now(),
281        })
282    }
283
284    /// Retry a failed workflow
285    ///
286    /// # Arguments
287    ///
288    /// * `run_id` - Workflow run ID to retry
289    ///
290    /// # Returns
291    ///
292    /// Result containing the retry result
293    pub async fn retry_workflow(&self, run_id: u64) -> Result<WorkflowRetryResult> {
294        if run_id == 0 {
295            return Err(GitHubError::invalid_input("Run ID cannot be zero"));
296        }
297
298        // In a real implementation, this would call the GitHub API to re-run the workflow
299        // For now, we return a mock result
300        Ok(WorkflowRetryResult {
301            new_run_id: run_id + 1,
302            new_run_number: 43,
303            status: WorkflowStatus::Queued,
304            html_url: format!(
305                "https://github.com/{}/{}/actions/runs/{}",
306                self.owner,
307                self.repo,
308                run_id + 1
309            ),
310        })
311    }
312
313    /// Summarize CI results
314    ///
315    /// # Arguments
316    ///
317    /// * `run_id` - Workflow run ID
318    ///
319    /// # Returns
320    ///
321    /// Result containing the CI result summary
322    pub async fn summarize_ci_results(&self, run_id: u64) -> Result<CiResultSummary> {
323        if run_id == 0 {
324            return Err(GitHubError::invalid_input("Run ID cannot be zero"));
325        }
326
327        // In a real implementation, this would aggregate workflow data
328        // For now, we return a mock result
329        Ok(CiResultSummary {
330            run_id,
331            status: WorkflowStatus::Completed,
332            conclusion: Some("success".to_string()),
333            total_jobs: 5,
334            passed_jobs: 5,
335            failed_jobs: 0,
336            skipped_jobs: 0,
337            duration_seconds: 120,
338            key_findings: vec!["All tests passed".to_string()],
339            recommendations: vec![],
340        })
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_actions_integration_creation() {
350        let actions = ActionsIntegration::new(
351            "token".to_string(),
352            "owner".to_string(),
353            "repo".to_string(),
354        );
355        assert_eq!(actions.owner, "owner");
356        assert_eq!(actions.repo, "repo");
357    }
358
359    #[tokio::test]
360    async fn test_trigger_workflow_with_empty_workflow() {
361        let actions = ActionsIntegration::new(
362            "token".to_string(),
363            "owner".to_string(),
364            "repo".to_string(),
365        );
366        let request = WorkflowTriggerRequest {
367            workflow: String::new(),
368            ref_branch: "main".to_string(),
369            inputs: HashMap::new(),
370        };
371        let result = actions.trigger_workflow(request).await;
372        assert!(result.is_err());
373    }
374
375    #[tokio::test]
376    async fn test_trigger_workflow_with_empty_branch() {
377        let actions = ActionsIntegration::new(
378            "token".to_string(),
379            "owner".to_string(),
380            "repo".to_string(),
381        );
382        let request = WorkflowTriggerRequest {
383            workflow: "test.yml".to_string(),
384            ref_branch: String::new(),
385            inputs: HashMap::new(),
386        };
387        let result = actions.trigger_workflow(request).await;
388        assert!(result.is_err());
389    }
390
391    #[tokio::test]
392    async fn test_trigger_workflow_success() {
393        let actions = ActionsIntegration::new(
394            "token".to_string(),
395            "owner".to_string(),
396            "repo".to_string(),
397        );
398        let request = WorkflowTriggerRequest {
399            workflow: "test.yml".to_string(),
400            ref_branch: "main".to_string(),
401            inputs: HashMap::new(),
402        };
403        let result = actions.trigger_workflow(request).await;
404        assert!(result.is_ok());
405        let trigger_result = result.unwrap();
406        assert_eq!(trigger_result.status, WorkflowStatus::Queued);
407    }
408
409    #[tokio::test]
410    async fn test_track_workflow_status_with_zero_id() {
411        let actions = ActionsIntegration::new(
412            "token".to_string(),
413            "owner".to_string(),
414            "repo".to_string(),
415        );
416        let result = actions.track_workflow_status(0).await;
417        assert!(result.is_err());
418    }
419
420    #[tokio::test]
421    async fn test_track_workflow_status_success() {
422        let actions = ActionsIntegration::new(
423            "token".to_string(),
424            "owner".to_string(),
425            "repo".to_string(),
426        );
427        let result = actions.track_workflow_status(12345).await;
428        assert!(result.is_ok());
429        let status = result.unwrap();
430        assert_eq!(status.run_id, 12345);
431    }
432
433    #[tokio::test]
434    async fn test_diagnose_ci_failure_with_zero_id() {
435        let actions = ActionsIntegration::new(
436            "token".to_string(),
437            "owner".to_string(),
438            "repo".to_string(),
439        );
440        let result = actions.diagnose_ci_failure(0).await;
441        assert!(result.is_err());
442    }
443
444    #[tokio::test]
445    async fn test_diagnose_ci_failure_success() {
446        let actions = ActionsIntegration::new(
447            "token".to_string(),
448            "owner".to_string(),
449            "repo".to_string(),
450        );
451        let result = actions.diagnose_ci_failure(12345).await;
452        assert!(result.is_ok());
453        let diagnostics = result.unwrap();
454        assert!(!diagnostics.recommendations.is_empty());
455    }
456
457    #[tokio::test]
458    async fn test_retry_workflow_with_zero_id() {
459        let actions = ActionsIntegration::new(
460            "token".to_string(),
461            "owner".to_string(),
462            "repo".to_string(),
463        );
464        let result = actions.retry_workflow(0).await;
465        assert!(result.is_err());
466    }
467
468    #[tokio::test]
469    async fn test_retry_workflow_success() {
470        let actions = ActionsIntegration::new(
471            "token".to_string(),
472            "owner".to_string(),
473            "repo".to_string(),
474        );
475        let result = actions.retry_workflow(12345).await;
476        assert!(result.is_ok());
477        let retry_result = result.unwrap();
478        assert_eq!(retry_result.new_run_id, 12346);
479    }
480
481    #[tokio::test]
482    async fn test_summarize_ci_results_with_zero_id() {
483        let actions = ActionsIntegration::new(
484            "token".to_string(),
485            "owner".to_string(),
486            "repo".to_string(),
487        );
488        let result = actions.summarize_ci_results(0).await;
489        assert!(result.is_err());
490    }
491
492    #[tokio::test]
493    async fn test_summarize_ci_results_success() {
494        let actions = ActionsIntegration::new(
495            "token".to_string(),
496            "owner".to_string(),
497            "repo".to_string(),
498        );
499        let result = actions.summarize_ci_results(12345).await;
500        assert!(result.is_ok());
501        let summary = result.unwrap();
502        assert_eq!(summary.run_id, 12345);
503        assert_eq!(summary.total_jobs, 5);
504    }
505}