1use crate::errors::{GitHubError, Result};
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum WorkflowStatus {
14 Queued,
16 InProgress,
18 Completed,
20 Failed,
22 Cancelled,
24 Skipped,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct WorkflowRun {
31 pub id: u64,
33 pub run_number: u32,
35 pub name: String,
37 pub status: WorkflowStatus,
39 pub conclusion: Option<String>,
41 pub head_branch: String,
43 pub head_sha: String,
45 pub created_at: DateTime<Utc>,
47 pub updated_at: DateTime<Utc>,
49 pub html_url: String,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct WorkflowJob {
56 pub id: u64,
58 pub name: String,
60 pub status: WorkflowStatus,
62 pub conclusion: Option<String>,
64 pub started_at: DateTime<Utc>,
66 pub completed_at: Option<DateTime<Utc>>,
68 pub steps: Vec<JobStep>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct JobStep {
75 pub name: String,
77 pub status: WorkflowStatus,
79 pub conclusion: Option<String>,
81 pub output: Option<String>,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct CiFailureDiagnostics {
88 pub failed_jobs: Vec<WorkflowJob>,
90 pub error_logs: Vec<String>,
92 pub failed_steps: Vec<JobStep>,
94 pub recommendations: Vec<String>,
96 pub diagnosed_at: DateTime<Utc>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct WorkflowTriggerRequest {
103 pub workflow: String,
105 pub ref_branch: String,
107 pub inputs: HashMap<String, String>,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct WorkflowTriggerResult {
114 pub run_id: u64,
116 pub run_number: u32,
118 pub status: WorkflowStatus,
120 pub html_url: String,
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126pub struct WorkflowStatusResult {
127 pub run_id: u64,
129 pub status: WorkflowStatus,
131 pub conclusion: Option<String>,
133 pub progress: u8,
135 pub jobs: Vec<WorkflowJob>,
137 pub updated_at: DateTime<Utc>,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143pub struct WorkflowRetryResult {
144 pub new_run_id: u64,
146 pub new_run_number: u32,
148 pub status: WorkflowStatus,
150 pub html_url: String,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct CiResultSummary {
157 pub run_id: u64,
159 pub status: WorkflowStatus,
161 pub conclusion: Option<String>,
163 pub total_jobs: u32,
165 pub passed_jobs: u32,
167 pub failed_jobs: u32,
169 pub skipped_jobs: u32,
171 pub duration_seconds: u64,
173 pub key_findings: Vec<String>,
175 pub recommendations: Vec<String>,
177}
178
179pub struct ActionsIntegration {
181 #[allow(dead_code)]
183 pub token: String,
184 pub owner: String,
186 pub repo: String,
188}
189
190impl ActionsIntegration {
191 pub fn new(token: String, owner: String, repo: String) -> Self {
193 Self { token, owner, repo }
194 }
195
196 pub async fn trigger_workflow(
206 &self,
207 request: WorkflowTriggerRequest,
208 ) -> Result<WorkflowTriggerResult> {
209 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 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 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 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 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 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 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 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 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 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}