releasaurus 0.20.0-rc.2

A comprehensive release automation tool that streamlines the software release process across multiple programming languages and forge platforms
Documentation
//! # Releasaurus
//!
//! A comprehensive release automation tool that streamlines the software
//! release process across multiple programming languages and forge platforms.
//!
//! ## Overview
//!
//! Releasaurus automates the entire release workflow including:
//! - Version detection and bumping across different project types
//! - Changelog generation
//! - Creating release pull requests
//! - Tagging and Publishing releases to various forge platforms
//!   (GitHub, GitLab, Gitea)
//!
//! See complete documentation at <https://releasaurus.rgon.io>

use clap::Parser;
use color_eyre::eyre::Result;
use releasaurus::cli::{Cli, Command, GetCommand, get};
use releasaurus_core::config::resolved::PackageOverrides;
use releasaurus_core::forge::manager::{ForgeManager, ForgeOptions};
use releasaurus_core::orchestrator::Orchestrator;
use releasaurus_core::resolver::Resolver;
use std::collections::HashMap;
use std::rc::Rc;

const DEBUG_ENV_VAR: &str = "RELEASAURUS_DEBUG";
const DRY_RUN_ENV_VAR: &str = "RELEASAURUS_DRY_RUN";

fn silence_logs(cli: &Cli) -> bool {
    let mut silent = false;

    if let Command::Get { command, .. } = &cli.command {
        match command {
            GetCommand::NextRelease { out_file, .. } => {
                if out_file.is_none() {
                    silent = true;
                }
            }
            GetCommand::CurrentRelease { out_file, .. } => {
                if out_file.is_none() {
                    silent = true;
                }
            }
            GetCommand::Release { out_file, .. } => {
                if out_file.is_none() {
                    silent = true;
                }
            }
            GetCommand::RecompiledNotes { out_file, .. } => {
                if out_file.is_none() {
                    silent = true;
                }
            }
        }
    }

    silent
}

/// Initialize terminal logger with debug or info level filtering for
/// releasaurus output.
fn initialize_logger(cli: &Cli) -> Result<()> {
    let silent = silence_logs(cli);

    let filter = if silent {
        simplelog::LevelFilter::Off
    } else if cli.debug {
        simplelog::LevelFilter::Debug
    } else {
        simplelog::LevelFilter::Info
    };

    let config = simplelog::ConfigBuilder::new()
        .add_filter_allow_str("releasaurus")
        .build();

    simplelog::TermLogger::init(
        filter,
        config,
        simplelog::TerminalMode::Mixed,
        simplelog::ColorChoice::Auto,
    )?;

    Ok(())
}

fn get_dry_run_value(cli: &Cli) -> bool {
    if std::env::var(DRY_RUN_ENV_VAR).is_ok() {
        return true;
    }

    match cli.command {
        Command::Release { dry_run, .. } => dry_run,
        Command::ReleasePR { dry_run, .. } => dry_run,
        Command::StartNext { dry_run, .. } => dry_run,
        _ => false,
    }
}

async fn create_orchestrator(cli: &Cli, dry_run: bool) -> Result<Orchestrator> {
    let mut forge = cli.forge_args.forge().await?;

    let global_overrides = cli.get_global_overrides();
    let package_overrides = cli.get_package_overrides()?;
    let commit_modifiers = cli.get_commit_modifiers();

    let config = Rc::new(
        forge
            .load_config(global_overrides.base_branch.clone())
            .await?,
    );

    forge.set_commit_search_depth(config.first_release_search_depth);
    forge.set_tag_search_depth(config.tag_search_depth);

    let forge_manager = ForgeManager::new(forge, ForgeOptions { dry_run });

    log::debug!("global overrides: {:#?}", global_overrides);
    log::debug!("package overrides: {:#?}", package_overrides);
    log::debug!("commit modifiers: {:#?}", commit_modifiers);

    let repo_name = forge_manager.repo_name();
    let default_branch = forge_manager.default_branch();
    let release_link_base_url = forge_manager.release_link_base_url();
    let compare_link_base_url = forge_manager.compare_link_base_url();

    let resolver = Resolver::builder()
        .commit_modifiers(commit_modifiers)
        .global_overrides(global_overrides)
        .package_overrides(
            package_overrides
                .into_iter()
                .map(|(k, v)| (k, PackageOverrides::from(v)))
                .collect::<HashMap<_, _>>(),
        )
        .release_link_base_url(release_link_base_url.clone())
        .compare_link_base_url(compare_link_base_url.clone())
        .repo_default_branch(default_branch)
        .repo_name(repo_name)
        .toml_config(Rc::clone(&config))
        .build()?;

    let (resolved_config, resolved_packages) =
        resolver.resolve(config.packages.clone())?;

    let orchestrator = Orchestrator::builder()
        .config(Rc::clone(&resolved_config))
        .package_configs(Rc::new(resolved_packages))
        .forge(Rc::new(forge_manager))
        .build()?;

    Ok(orchestrator)
}

/// Main entry point that initializes error handling, logging, and dispatches
/// commands.
#[tokio::main]
async fn main() -> Result<()> {
    color_eyre::install()?;

    let mut cli = Cli::parse();

    if std::env::var(DEBUG_ENV_VAR).is_ok() {
        cli.debug = true;
    }

    let dry_run = get_dry_run_value(&cli);

    if dry_run {
        cli.debug = true;
    }

    initialize_logger(&cli)?;

    let orchestrator = create_orchestrator(&cli, dry_run).await?;

    // wrap all errors using ? and manually return Ok(()) to get the benefit
    // of eyre Report
    match cli.command {
        Command::ReleasePR { package, .. } => {
            orchestrator.create_release_prs(package).await?;
            Ok(())
        }
        Command::Release { package, .. } => {
            orchestrator.create_releases(package).await?;
            Ok(())
        }
        Command::Get { command } => {
            get::execute(orchestrator, command).await?;
            Ok(())
        }
        Command::StartNext { packages, .. } => {
            orchestrator.start_next_release(packages).await?;
            Ok(())
        }
    }
}

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

    fn create_base_args() -> Vec<String> {
        vec![
            "releasaurus".to_string(),
            "--repo".to_string(),
            "https://github.com/test/repo".to_string(),
        ]
    }

    #[test]
    fn silence_logs_returns_true_for_get_next_release_without_out_file() {
        let args = [
            create_base_args(),
            vec!["get".to_string(), "next-release".to_string()],
        ]
        .concat();
        let cli = Cli::try_parse_from(args).unwrap();

        assert!(silence_logs(&cli));
    }

    #[test]
    fn silence_logs_returns_false_for_get_next_release_with_out_file() {
        let args = [
            create_base_args(),
            vec![
                "get".to_string(),
                "next-release".to_string(),
                "--out-file".to_string(),
                "output.json".to_string(),
            ],
        ]
        .concat();
        let cli = Cli::try_parse_from(args).unwrap();

        assert!(!silence_logs(&cli));
    }

    #[test]
    fn silence_logs_returns_true_for_get_current_release_without_out_file() {
        let args = [
            create_base_args(),
            vec!["get".to_string(), "current-release".to_string()],
        ]
        .concat();
        let cli = Cli::try_parse_from(args).unwrap();

        assert!(silence_logs(&cli));
    }

    #[test]
    fn silence_logs_returns_false_for_get_current_release_with_out_file() {
        let args = [
            create_base_args(),
            vec![
                "get".to_string(),
                "current-release".to_string(),
                "--out-file".to_string(),
                "output.json".to_string(),
            ],
        ]
        .concat();
        let cli = Cli::try_parse_from(args).unwrap();

        assert!(!silence_logs(&cli));
    }

    #[test]
    fn silence_logs_returns_true_for_get_release_without_out_file() {
        let args = [
            create_base_args(),
            vec![
                "get".to_string(),
                "release".to_string(),
                "--tag".to_string(),
                "v1.0.0".to_string(),
            ],
        ]
        .concat();
        let cli = Cli::try_parse_from(args).unwrap();

        assert!(silence_logs(&cli));
    }

    #[test]
    fn silence_logs_returns_false_for_get_release_with_out_file() {
        let args = [
            create_base_args(),
            vec![
                "get".to_string(),
                "release".to_string(),
                "--tag".to_string(),
                "v1.0.0".to_string(),
                "--out-file".to_string(),
                "output.json".to_string(),
            ],
        ]
        .concat();
        let cli = Cli::try_parse_from(args).unwrap();

        assert!(!silence_logs(&cli));
    }

    #[test]
    fn silence_logs_returns_true_for_get_notes_without_out_file() {
        let args = [
            create_base_args(),
            vec![
                "get".to_string(),
                "notes".to_string(),
                "--file".to_string(),
                "releases.json".to_string(),
            ],
        ]
        .concat();
        let cli = Cli::try_parse_from(args).unwrap();

        assert!(silence_logs(&cli));
    }

    #[test]
    fn silence_logs_returns_false_for_get_notes_with_out_file() {
        let args = [
            create_base_args(),
            vec![
                "get".to_string(),
                "notes".to_string(),
                "--file".to_string(),
                "releases.json".to_string(),
                "--out-file".to_string(),
                "output.json".to_string(),
            ],
        ]
        .concat();
        let cli = Cli::try_parse_from(args).unwrap();

        assert!(!silence_logs(&cli));
    }

    #[test]
    fn silence_logs_returns_false_for_non_get_commands() {
        let test_cases = vec!["release-pr", "release", "start-next"];

        for cmd in test_cases {
            let args = [create_base_args(), vec![cmd.to_string()]].concat();
            let cli = Cli::try_parse_from(args).unwrap();

            assert!(
                !silence_logs(&cli),
                "silence_logs should return false for {} command",
                cmd
            );
        }
    }
}