use ggen_utils::error::Result;
use std::str::FromStr;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkflowStatus {
Queued,
InProgress,
Completed,
Failed,
Cancelled,
}
impl FromStr for WorkflowStatus {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"queued" => Ok(Self::Queued),
"in_progress" => Ok(Self::InProgress),
"completed" => Ok(Self::Completed),
"failed" => Ok(Self::Failed),
"cancelled" => Ok(Self::Cancelled),
_ => Err(format!("Invalid workflow status: {}", s)),
}
}
}
impl WorkflowStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Queued => "queued",
Self::InProgress => "in_progress",
Self::Completed => "completed",
Self::Failed => "failed",
Self::Cancelled => "cancelled",
}
}
pub fn is_active(&self) -> bool {
matches!(self, Self::Queued | Self::InProgress)
}
}
#[derive(Debug, Clone)]
pub struct WorkflowInfo {
pub id: String,
pub name: String,
pub status: WorkflowStatus,
pub conclusion: Option<String>,
pub created_at: String,
pub updated_at: String,
pub html_url: String,
}
#[derive(Debug, Clone)]
pub struct WorkflowListResult {
pub workflows: Vec<WorkflowInfo>,
pub total_count: usize,
}
#[derive(Debug, Clone)]
pub struct WorkflowStatusResult {
pub workflow: WorkflowInfo,
pub jobs: Vec<JobInfo>,
}
#[derive(Debug, Clone)]
pub struct JobInfo {
pub id: String,
pub name: String,
pub status: WorkflowStatus,
pub conclusion: Option<String>,
pub started_at: Option<String>,
pub completed_at: Option<String>,
}
#[derive(Debug, Clone)]
pub struct WorkflowLogsResult {
pub logs: String,
pub workflow_id: String,
}
pub trait WorkflowManager {
fn list(&self, active_only: bool) -> Result<WorkflowListResult>;
fn cancel(&self, workflow_id: &str) -> Result<()>;
fn cancel_all(&self) -> Result<usize>;
}
pub trait WorkflowStatusChecker {
fn get_status(&self, workflow_id: Option<&str>) -> Result<WorkflowStatusResult>;
}
pub trait WorkflowLogViewer {
fn get_logs(&self, workflow_id: Option<&str>) -> Result<WorkflowLogsResult>;
fn stream_logs(&self, workflow_id: &str, callback: Box<dyn Fn(&str)>) -> Result<()>;
}
pub struct GhWorkflowManager;
impl WorkflowManager for GhWorkflowManager {
fn list(&self, active_only: bool) -> Result<WorkflowListResult> {
let mut cmd = std::process::Command::new("gh");
cmd.args([
"run",
"list",
"--json",
"databaseId,name,status,conclusion,createdAt,updatedAt,url",
]);
if active_only {
cmd.args(["--status", "in_progress,queued"]);
}
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ggen_utils::error::Error::new(&format!(
"Failed to list workflows: {}",
stderr
)));
}
let workflows = vec![];
Ok(WorkflowListResult {
workflows,
total_count: 0,
})
}
fn cancel(&self, workflow_id: &str) -> Result<()> {
let mut cmd = std::process::Command::new("gh");
cmd.args(["run", "cancel", workflow_id]);
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ggen_utils::error::Error::new(&format!(
"Failed to cancel workflow {}: {}",
workflow_id, stderr
)));
}
Ok(())
}
fn cancel_all(&self) -> Result<usize> {
let active = self.list(true)?;
let mut cancelled = 0;
for workflow in &active.workflows {
if self.cancel(&workflow.id).is_ok() {
cancelled += 1;
}
}
Ok(cancelled)
}
}
pub struct GhWorkflowStatusChecker;
impl WorkflowStatusChecker for GhWorkflowStatusChecker {
fn get_status(&self, workflow_id: Option<&str>) -> Result<WorkflowStatusResult> {
let mut cmd = std::process::Command::new("gh");
cmd.args(["run", "view"]);
if let Some(id) = workflow_id {
cmd.arg(id);
}
cmd.args([
"--json",
"databaseId,name,status,conclusion,createdAt,updatedAt,url,jobs",
]);
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ggen_utils::error::Error::new(&format!(
"Failed to get workflow status: {}",
stderr
)));
}
let workflow = WorkflowInfo {
id: "1".to_string(),
name: "test".to_string(),
status: WorkflowStatus::Completed,
conclusion: Some("success".to_string()),
created_at: "2024-01-01".to_string(),
updated_at: "2024-01-01".to_string(),
html_url: "https://github.com".to_string(),
};
Ok(WorkflowStatusResult {
workflow,
jobs: vec![],
})
}
}
pub struct GhWorkflowLogViewer;
impl WorkflowLogViewer for GhWorkflowLogViewer {
fn get_logs(&self, workflow_id: Option<&str>) -> Result<WorkflowLogsResult> {
let mut cmd = std::process::Command::new("gh");
cmd.args(["run", "view"]);
if let Some(id) = workflow_id {
cmd.arg(id);
}
cmd.arg("--log");
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ggen_utils::error::Error::new(&format!(
"Failed to get workflow logs: {}",
stderr
)));
}
let logs = String::from_utf8_lossy(&output.stdout).to_string();
Ok(WorkflowLogsResult {
logs,
workflow_id: workflow_id.unwrap_or("latest").to_string(),
})
}
fn stream_logs(&self, workflow_id: &str, callback: Box<dyn Fn(&str)>) -> Result<()> {
let mut cmd = std::process::Command::new("gh");
cmd.args(["run", "watch", workflow_id]);
let output = cmd.output()?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(ggen_utils::error::Error::new(&format!(
"Failed to stream logs: {}",
stderr
)));
}
let logs = String::from_utf8_lossy(&output.stdout);
callback(&logs);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_workflow_status_from_str() {
assert_eq!(
"queued".parse::<WorkflowStatus>().ok(),
Some(WorkflowStatus::Queued)
);
assert_eq!(
"IN_PROGRESS".parse::<WorkflowStatus>().ok(),
Some(WorkflowStatus::InProgress)
);
assert_eq!(
"completed".parse::<WorkflowStatus>().ok(),
Some(WorkflowStatus::Completed)
);
assert_eq!(
"failed".parse::<WorkflowStatus>().ok(),
Some(WorkflowStatus::Failed)
);
assert_eq!(
"cancelled".parse::<WorkflowStatus>().ok(),
Some(WorkflowStatus::Cancelled)
);
assert_eq!("unknown".parse::<WorkflowStatus>().ok(), None);
}
#[test]
fn test_workflow_status_as_str() {
assert_eq!(WorkflowStatus::Queued.as_str(), "queued");
assert_eq!(WorkflowStatus::InProgress.as_str(), "in_progress");
assert_eq!(WorkflowStatus::Completed.as_str(), "completed");
assert_eq!(WorkflowStatus::Failed.as_str(), "failed");
assert_eq!(WorkflowStatus::Cancelled.as_str(), "cancelled");
}
#[test]
fn test_workflow_status_is_active() {
assert!(WorkflowStatus::Queued.is_active());
assert!(WorkflowStatus::InProgress.is_active());
assert!(!WorkflowStatus::Completed.is_active());
assert!(!WorkflowStatus::Failed.is_active());
assert!(!WorkflowStatus::Cancelled.is_active());
}
}