Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 52 additions & 24 deletions src/cortex-cli/src/github_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand Down Expand Up @@ -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());
}
}

Expand Down Expand Up @@ -695,14 +696,18 @@ 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;
}
}
}

if found_workflow.is_none() {
found_workflow = find_cortex_workflow(&workflows_dir)?;
}

let workflow_path = match found_workflow {
Some(p) => p,
None => {
Expand Down Expand Up @@ -827,6 +832,29 @@ struct InstallationStatus {
features: Vec<String>,
}

fn find_cortex_workflow(workflows_dir: &std::path::Path) -> Result<Option<PathBuf>> {
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::*;
Expand Down
96 changes: 96 additions & 0 deletions src/cortex-cli/tests/github_workflow_detection.rs
Original file line number Diff line number Diff line change
@@ -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()
);
}