forge-guard 0.3.6

Pre-deployment smart contract auditing framework for Foundry
Documentation
//! `forge-guard ci` — generate CI/CD pipeline configurations.

use crate::ci::CiGenerator;
use crate::core::ProjectConfig;

use super::CiArgs;
use anyhow::{Context, Result};
use colored::*;

/// Generate CI/CD pipeline configurations.
pub fn run(args: &CiArgs) -> Result<()> {
    eprintln!("{}", "🔧 Forge Guard — CI/CD Generator".bold());

    if args.vscode {
        return run_vscode(args);
    }

    let generator = CiGenerator::new(&args.platform);

    eprintln!("   Platform: {}", args.platform.bold());

    // Generate the CI config
    let config = generator.generate(args.include_deploy)?;

    // Write to output directory
    let output_path = args.output.join(generator.filename());
    std::fs::create_dir_all(&args.output)?;

    if output_path.exists() && !args.overwrite {
        anyhow::bail!(
            "{} already exists. Use --overwrite to replace.",
            output_path.display()
        );
    }

    std::fs::write(&output_path, &config).context("Failed to write CI config")?;

    eprintln!(
        "{} CI config written to {}",
        "".green(),
        output_path.display()
    );
    println!("{}", config);

    Ok(())
}

/// Generate VS Code extension config (`.vscode/settings.json`, `tasks.json`,
/// `extensions.json`) — `forge-guard ci --vscode`.
fn run_vscode(args: &CiArgs) -> Result<()> {
    use crate::ci::vscode;

    // Load the project config so the Solidity validation rules can pin the
    // real compiler version and remappings (synced from foundry.toml via
    // `doctor --sync`). Defaults are used when no forge-guard.toml exists.
    let mut config = ProjectConfig::from_default_location();
    config.project_root = args.shared.project.clone();

    let vscode_dir = config.project_root.join(".vscode");
    std::fs::create_dir_all(&vscode_dir)?;

    let files = [
        (
            "settings.json",
            vscode::generate_settings(config.solc_version.as_deref(), &config.remappings),
        ),
        ("tasks.json", vscode::generate_tasks()),
        ("extensions.json", vscode::generate_extensions()),
    ];

    eprintln!(
        "   Mode:      {} (VS Code extension config)",
        "vscode".bold()
    );

    for (name, contents) in files {
        let path = vscode_dir.join(name);
        if path.exists() && !args.overwrite {
            anyhow::bail!(
                "{} already exists. Use --overwrite to replace.",
                path.display()
            );
        }
        std::fs::write(&path, contents).context("Failed to write VS Code config")?;
        eprintln!("{} {} written", "".green(), path.display());
    }

    eprintln!(
        "\n{} Restart VS Code (or reload the window) to pick up the new config.",
        "💡".cyan()
    );
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_ci_arg_defaults() {
        let args = CiArgs {
            shared: super::super::SharedFlags {
                chain: "ethereum".into(),
                project: std::path::PathBuf::from("."),
                json: false,
                markdown: false,
                html: false,
                strict: false,
                offline: false,
                production: false,
                report: false,
                parallelism: 4,
            },
            platform: "github".into(),
            output: std::path::PathBuf::from(".github/workflows"),
            include_deploy: false,
            vscode: false,
            overwrite: false,
        };
        assert_eq!(args.platform, "github");
        assert_eq!(args.output, std::path::PathBuf::from(".github/workflows"));
        assert!(!args.include_deploy);
        assert!(!args.vscode);
        assert!(!args.overwrite);
    }

    #[test]
    fn test_ci_generator_can_create_instances() {
        let gen = CiGenerator::new("github");
        assert_eq!(gen.filename(), "audit.yml");
    }

    #[test]
    fn test_ci_generator_gitlab_filename() {
        let gen = CiGenerator::new("gitlab");
        assert_eq!(gen.filename(), ".gitlab-ci.yml");
    }

    #[test]
    fn test_ci_generator_unsupported_platform_errs() {
        let gen = CiGenerator::new("circleci");
        let result = gen.generate(false);
        assert!(result.is_err());
    }
}