diff --git a/src/cortex-cli/src/github_cmd.rs b/src/cortex-cli/src/github_cmd.rs index 83763b3b..14f79d40 100644 --- a/src/cortex-cli/src/github_cmd.rs +++ b/src/cortex-cli/src/github_cmd.rs @@ -208,6 +208,16 @@ async fn run_install(args: InstallArgs) -> Result<()> { let workflows_dir = canonical_path.join(".github").join("workflows"); let workflow_file = workflows_dir.join(format!("{}.yml", args.workflow_name)); + if let Some(existing_workflow) = find_cortex_workflow(&workflows_dir)? + && existing_workflow != workflow_file + && !args.force + { + bail!( + "Cortex workflow already exists: {}\nUse `cortex github update` to update it, `cortex github uninstall` to remove it, or --force to create another workflow.", + existing_workflow.display() + ); + } + // Check if workflow already exists if workflow_file.exists() && !args.force { bail!( @@ -592,29 +602,20 @@ async fn run_status(args: StatusArgs) -> Result<()> { // Check for workflow files let workflows_dir = repo_path.join(".github").join("workflows"); - if workflows_dir.exists() { - for entry in std::fs::read_dir(&workflows_dir)? { - let entry = entry?; - let path = entry.path(); - if path.extension().is_some_and(|e| e == "yml" || e == "yaml") { - let content = std::fs::read_to_string(&path)?; - if content.contains("Cortex") { - status.workflow_installed = true; - status.workflow_path = Some(path.clone()); - - // Check workflow features - if content.contains("issue_comment") { - status.features.push("issue_comment".to_string()); - } - if content.contains("pull_request") { - status.features.push("pull_request".to_string()); - } - if content.contains("issues") { - status.features.push("issues".to_string()); - } - break; - } - } + if let Some(path) = find_cortex_workflow(&workflows_dir)? { + let content = std::fs::read_to_string(&path)?; + status.workflow_installed = true; + status.workflow_path = Some(path); + + // Check workflow features + if content.contains("issue_comment") { + status.features.push("issue_comment".to_string()); + } + if content.contains("pull_request") { + status.features.push("pull_request".to_string()); + } + if content.contains("issues") { + status.features.push("issues".to_string()); } } @@ -695,7 +696,7 @@ async fn run_uninstall(args: UninstallArgs) -> Result<()> { if path.exists() { // Verify it's a Cortex workflow if let Ok(content) = std::fs::read_to_string(&path) - && (content.contains("Cortex") || content.contains("cortex")) + && is_cortex_generated_workflow(&content) { found_workflow = Some(path); break; @@ -703,6 +704,10 @@ async fn run_uninstall(args: UninstallArgs) -> Result<()> { } } + if found_workflow.is_none() { + found_workflow = find_cortex_workflow(&workflows_dir)?; + } + let workflow_path = match found_workflow { Some(p) => p, None => { @@ -827,6 +832,29 @@ struct InstallationStatus { features: Vec, } +fn find_cortex_workflow(workflows_dir: &std::path::Path) -> Result> { + if !workflows_dir.exists() { + return Ok(None); + } + + for entry in std::fs::read_dir(workflows_dir)? { + let entry = entry?; + let path = entry.path(); + if path.extension().is_some_and(|e| e == "yml" || e == "yaml") { + let content = std::fs::read_to_string(&path)?; + if is_cortex_generated_workflow(&content) { + return Ok(Some(path)); + } + } + } + + Ok(None) +} + +fn is_cortex_generated_workflow(content: &str) -> bool { + content.contains("# Generated by: cortex github install") +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/cortex-cli/tests/github_workflow_detection.rs b/src/cortex-cli/tests/github_workflow_detection.rs new file mode 100644 index 00000000..c7e9b74c --- /dev/null +++ b/src/cortex-cli/tests/github_workflow_detection.rs @@ -0,0 +1,96 @@ +use std::fs; +use std::process::Command; + +use tempfile::tempdir; + +fn cortex() -> Command { + Command::new(env!("CARGO_BIN_EXE_Cortex")) +} + +fn write_generated_workflow(repo: &std::path::Path, name: &str) { + let workflows_dir = repo.join(".github").join("workflows"); + fs::create_dir_all(&workflows_dir).unwrap(); + fs::write( + workflows_dir.join(name), + r#"# Cortex CI/CD Automation +# Generated by: cortex github install + +name: Cortex + +on: + issue_comment: + types: [created] + workflow_dispatch: + +jobs: + cortex: + runs-on: ubuntu-latest + steps: [] +"#, + ) + .unwrap(); +} + +#[test] +fn status_install_and_uninstall_use_same_generated_workflow() { + let repo = tempdir().unwrap(); + fs::create_dir(repo.path().join(".git")).unwrap(); + write_generated_workflow(repo.path(), "release.yml"); + + let status_output = cortex() + .args(["github", "status", "--path", repo.path().to_str().unwrap()]) + .output() + .unwrap(); + let status_stdout = String::from_utf8_lossy(&status_output.stdout); + let status_stderr = String::from_utf8_lossy(&status_output.stderr); + assert!( + status_output.status.success(), + "status failed\nstdout:\n{status_stdout}\nstderr:\n{status_stderr}" + ); + assert!(status_stdout.contains("release.yml")); + + let install_output = cortex() + .args(["github", "install", "--path", repo.path().to_str().unwrap()]) + .output() + .unwrap(); + assert!( + !install_output.status.success(), + "install should reject duplicate workflow\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&install_output.stdout), + String::from_utf8_lossy(&install_output.stderr) + ); + assert!( + !repo + .path() + .join(".github") + .join("workflows") + .join("Cortex.yml") + .exists(), + "install should not create a duplicate Cortex.yml" + ); + + let uninstall_output = cortex() + .args([ + "github", + "uninstall", + "--path", + repo.path().to_str().unwrap(), + "--force", + ]) + .output() + .unwrap(); + let uninstall_stdout = String::from_utf8_lossy(&uninstall_output.stdout); + let uninstall_stderr = String::from_utf8_lossy(&uninstall_output.stderr); + assert!( + uninstall_output.status.success(), + "uninstall failed\nstdout:\n{uninstall_stdout}\nstderr:\n{uninstall_stderr}" + ); + assert!( + !repo + .path() + .join(".github") + .join("workflows") + .join("release.yml") + .exists() + ); +}