pmat 3.11.0

PMAT - Zero-config AI context generation and code quality toolkit (CLI, MCP, HTTP)
/// Generate issue templates from categorization
pub fn generate_issue_templates(
    report: &CategorizationReport,
    labels: Option<Vec<String>>,
) -> Vec<TestIssueTemplate> {
    let default_labels = vec!["test-quality".to_string(), "automated".to_string()];
    let extra_labels = labels.unwrap_or_default();

    report
        .groups
        .iter()
        .map(|group| {
            let test_list = group
                .tests
                .iter()
                .take(20) // Limit to avoid huge issues
                .map(|t| format!("- `{}`", t.name))
                .collect::<Vec<_>>()
                .join("\n");

            let more_tests = if group.tests.len() > 20 {
                format!("\n... and {} more tests", group.tests.len() - 20)
            } else {
                String::new()
            };

            let body = format!(
                r#"## Root Cause
{}

## Affected Tests ({} total)
{}{}

## Suggested Fix
{}

## Priority
{} (1=fix now, 5=low priority)

---
*Generated by `pmat test-discovery create-tickets`*
"#,
                group.root_cause,
                group.tests.len(),
                test_list,
                more_tests,
                group.ignore_reason,
                group.priority
            );

            let mut all_labels = default_labels.clone();
            all_labels.extend(extra_labels.clone());
            all_labels.push(format!("priority-{}", group.priority));

            TestIssueTemplate {
                title: format!("[Test Failures] {}", group.root_cause),
                body,
                labels: all_labels,
                category: group.root_cause.clone(),
                test_count: group.tests.len(),
            }
        })
        .collect()
}

/// Phase 5: Create tickets - Generate GitHub issues from categories
async fn handle_create_tickets(
    input: &Path,
    create: bool,
    output: Option<&Path>,
    repo: Option<&str>,
    labels: Option<Vec<String>>,
) -> Result<()> {
    println!("🎫 Creating tickets from categorized failures");
    if !create {
        println!("   (DRY RUN - use --create to create GitHub issues)");
    }
    println!();

    // Read categorization report
    let content = std::fs::read_to_string(input).context("Failed to read categorization report")?;
    let report: CategorizationReport =
        serde_json::from_str(&content).context("Failed to parse categorization report")?;

    println!("   Found {} failure groups", report.groups.len());

    // Generate issue templates
    let tickets = generate_issue_templates(&report, labels);

    // Create tickets report
    let tickets_report = TicketsReport {
        tickets: tickets.clone(),
        total_tests: report.total_failures,
        timestamp: chrono::Utc::now().to_rfc3339(),
    };

    // Write output if requested
    if let Some(output_path) = output {
        let json = serde_json::to_string_pretty(&tickets_report)?;
        std::fs::write(output_path, json)?;
        println!("   Wrote tickets to: {}", output_path.display());
    }

    // Print summary
    println!("\n📋 Generated {} issues:", tickets.len());
    for ticket in &tickets {
        println!("   📝 {} ({} tests)", ticket.title, ticket.test_count);
    }

    if create {
        if let Some(repo_name) = repo {
            println!("\n🚀 Creating GitHub issues in {}...", repo_name);
            for ticket in &tickets {
                match create_github_issue(repo_name, ticket).await {
                    Ok(url) => println!("   ✅ Created: {}", url),
                    Err(e) => println!("   ❌ Failed: {}", e),
                }
            }
        } else {
            println!("\n⚠️  No --repo specified. Use --repo owner/repo to create issues.");
        }
    } else {
        println!("\n   Run with --create --repo owner/repo to create GitHub issues");
    }

    Ok(())
}

/// Create a single GitHub issue using gh CLI
async fn create_github_issue(repo: &str, ticket: &TestIssueTemplate) -> Result<String> {
    let labels_arg = ticket.labels.join(",");

    let output = Command::new("gh")
        .arg("issue")
        .arg("create")
        .arg("--repo")
        .arg(repo)
        .arg("--title")
        .arg(&ticket.title)
        .arg("--body")
        .arg(&ticket.body)
        .arg("--label")
        .arg(&labels_arg)
        .output()
        .context("Failed to run gh CLI")?;

    if output.status.success() {
        let url = String::from_utf8_lossy(&output.stdout).trim().to_string();
        Ok(url)
    } else {
        let stderr = String::from_utf8_lossy(&output.stderr);
        anyhow::bail!("gh issue create failed: {}", stderr)
    }
}