rlyx 0.3.1

rlyx is a fast release manager that automatically bumps versions, creates changelogs, tags commits, and publishes GitHub releases across JS, Rust, and Python projects with first class monorepos support.
Documentation
use anyhow::Result;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub struct Config {
    pub path: PathBuf,
    pub changelog_enabled: bool,
    pub changelog_path: Option<String>,
    pub default_bump: String,
    pub bump_commit_message: String,
    pub bump_tag_message: Option<String>,
    pub changelog_header: Option<String>,
    pub pkg_tag_template: String,
    pub include_v_prefix: bool,
    pub releases_per_package: bool,
    pub umbrella_release: bool,
    pub monorepo_bump_commit_message: Option<String>,
    pub allow_dirty: bool,
    pub packages: Vec<String>,
    pub migration: BTreeMap<String, MigrationHints>,
    pub post_bump_command: Option<String>,
}

#[derive(Deserialize, Default)]
struct RcChangelog {
    pub enable: Option<bool>,
    pub path: Option<String>,
}

#[derive(Deserialize, Default)]
struct RawConfig {
    pub default_bump: Option<String>,
    pub bump_commit_message: Option<String>,
    pub bump_tag_message: Option<String>,
    pub changelog_header: Option<String>,
    pub changelog: Option<RcChangelog>,

    pub pkg_tag_template: Option<String>,
    pub include_v_prefix: Option<bool>,
    pub releases_per_package: Option<bool>,
    pub umbrella_release: Option<bool>,

    pub monorepo_bump_commit_message: Option<String>,
    pub allow_dirty: Option<bool>,
    pub packages: Option<Vec<String>>,
    pub migration: Option<BTreeMap<String, MigrationHints>>,

    pub post_bump_command: Option<String>,
}

#[derive(Deserialize, Default, Debug, Clone)]
pub struct MigrationHints {
    pub legacy_tag_patterns: Option<Vec<String>>,
}

fn env_bool(key: &str) -> Option<bool> {
    std::env::var(key).ok().and_then(|v| {
        let s = v.trim().to_ascii_lowercase();
        match s.as_str() {
            "1" | "true" | "yes" | "on" => Some(true),
            "0" | "false" | "no" | "off" => Some(false),
            _ => None,
        }
    })
}

fn env_str(key: &str) -> Option<String> {
    std::env::var(key)
        .ok()
        .map(|v| v.trim().to_string())
        .filter(|s| !s.is_empty())
}

pub fn load<P: AsRef<Path>>(path: P) -> Result<Config> {
    let mut path = path.as_ref().to_path_buf();
    if !path.exists() {
        let alt = Path::new("rlyx.toml");
        if alt.exists() {
            path = alt.to_path_buf();
        }
    }
    let mut raw: RawConfig = if path.exists() {
        let s = fs_err::read_to_string(&path)?;
        toml::from_str(&s).unwrap_or_default()
    } else {
        RawConfig::default()
    };

    if let Some(v) = env_str("RLYX_DEFAULT_BUMP") {
        raw.default_bump = Some(v);
    }
    if let Some(v) = env_str("RLYX_BUMP_COMMIT_MESSAGE") {
        raw.bump_commit_message = Some(v);
    }
    if let Some(v) = env_str("RLYX_BUMP_TAG_MESSAGE") {
        raw.bump_tag_message = Some(v);
    }
    if let Some(v) = env_str("RLYX_CHANGELOG_HEADER") {
        raw.changelog_header = Some(v);
    }
    if let Some(b) = env_bool("RLYX_CHANGELOG_ENABLE") {
        raw.changelog.get_or_insert(Default::default()).enable =
            Some(b);
    }
    if let Some(v) = env_str("RLYX_CHANGELOG_PATH") {
        raw.changelog.get_or_insert(Default::default()).path =
            Some(v);
    }
    if let Some(v) = env_str("RLYX_PKG_TAG_TEMPLATE") {
        raw.pkg_tag_template = Some(v);
    }
    if let Some(b) = env_bool("RLYX_INCLUDE_V_PREFIX") {
        raw.include_v_prefix = Some(b);
    }
    if let Some(b) = env_bool("RLYX_RELEASES_PER_PACKAGE") {
        raw.releases_per_package = Some(b);
    }
    if let Some(b) = env_bool("RLYX_UMBRELLA_RELEASE") {
        raw.umbrella_release = Some(b);
    }
    if let Some(v) = env_str("RLYX_MONOREPO_BUMP_COMMIT_MESSAGE") {
        raw.monorepo_bump_commit_message = Some(v);
    }
    if let Some(v) = env_str("RLYX_POST_BUMP_CMD") {
        raw.post_bump_command = Some(v);
    }

    let default_bump =
        raw.default_bump.unwrap_or_else(|| "patch".into());
    let bump_commit_message = raw
        .bump_commit_message
        .unwrap_or_else(|| "release v{{version}}".into());

    let monorepo_bump_commit_message =
        raw.monorepo_bump_commit_message.unwrap_or_else(|| {
            "release {{count}} packages\n\n{{list}}".into()
        });

    let (changelog_enabled, changelog_path) = {
        let en = raw
            .changelog
            .as_ref()
            .and_then(|c| c.enable)
            .unwrap_or(true);
        let p = raw.changelog.as_ref().and_then(|c| c.path.clone());
        (en, p)
    };

    let pkg_tag_template = raw
        .pkg_tag_template
        .unwrap_or_else(|| "{{name}}@v{{version}}".into());
    let include_v_prefix = raw.include_v_prefix.unwrap_or(true);
    let releases_per_package =
        raw.releases_per_package.unwrap_or(true);
    let umbrella_release = raw.umbrella_release.unwrap_or(false);
    let allow_dirty = raw.allow_dirty.unwrap_or(false);
    let packages = raw.packages.unwrap_or_default();
    let migration = raw.migration.unwrap_or_default();
    let post_bump_command = raw.post_bump_command;

    Ok(Config {
        path,
        changelog_enabled,
        changelog_path,
        default_bump,
        bump_commit_message,
        bump_tag_message: raw.bump_tag_message,
        changelog_header: raw.changelog_header,
        pkg_tag_template,
        include_v_prefix,
        releases_per_package,
        umbrella_release,
        monorepo_bump_commit_message: Some(
            monorepo_bump_commit_message,
        ),
        allow_dirty,
        packages,
        migration,
        post_bump_command,
    })
}