use crate::core::config::WorkspaceConfig;
use anyhow::{Context, Result};
pub fn cli_version() -> &'static str {
env!("CARGO_PKG_VERSION")
}
pub fn check_alef_toml_version(workspace: &WorkspaceConfig) -> Result<()> {
let Some(pin) = workspace.alef_version.as_deref() else {
return Ok(());
};
let cli = cli_version();
let (Ok(pin_v), Ok(cli_v)) = (semver::Version::parse(pin), semver::Version::parse(cli)) else {
tracing::warn!(
"alef.toml `[workspace] alef_version = \"{pin}\"` is not valid semver; running alef {cli} without changing the pin"
);
return Ok(());
};
match cli_v.cmp(&pin_v) {
std::cmp::Ordering::Greater => {
tracing::info!(
"Running alef {cli} is newer than the pinned alef_version {pin} in alef.toml; \
generation will not change the pin"
);
}
std::cmp::Ordering::Less => {
tracing::info!(
"Running alef {cli} is older than the pinned alef_version {pin} in alef.toml; \
generation will not change the pin"
);
}
std::cmp::Ordering::Equal => {}
}
Ok(())
}
pub fn maybe_update_alef_toml_version_pin(
workspace: &WorkspaceConfig,
config_path: &std::path::Path,
auto_update_enabled: bool,
running_build_is_clean: bool,
) -> Result<bool> {
if !auto_update_enabled {
return Ok(false);
}
let Some(pin) = workspace.alef_version.as_deref() else {
return Ok(false);
};
let cli = cli_version();
let (Ok(pin_v), Ok(cli_v)) = (semver::Version::parse(pin), semver::Version::parse(cli)) else {
return Ok(false);
};
if cli_v <= pin_v {
return Ok(false);
}
if !running_build_is_clean {
tracing::debug!(
"alef {cli} is newer than the pinned alef_version {pin} in alef.toml, but this \
build's working tree was not clean at compile time; leaving the pin unchanged"
);
return Ok(false);
}
write_alef_version_pin(config_path, cli)?;
tracing::info!("Updated alef.toml `[workspace] alef_version` pin from {pin} to {cli}");
Ok(true)
}
fn write_alef_version_pin(config_path: &std::path::Path, version: &str) -> Result<()> {
let content = std::fs::read_to_string(config_path)
.with_context(|| format!("reading {} to update the alef_version pin", config_path.display()))?;
let mut doc = content
.parse::<toml_edit::DocumentMut>()
.with_context(|| format!("parsing {} to update the alef_version pin", config_path.display()))?;
let workspace_item = doc.entry("workspace").or_insert(toml_edit::table());
let Some(workspace_table) = workspace_item.as_table_mut() else {
anyhow::bail!(
"{} has a non-table [workspace] entry; cannot update alef_version",
config_path.display()
);
};
workspace_table["alef_version"] = toml_edit::value(version);
std::fs::write(config_path, doc.to_string())
.with_context(|| format!("writing {} after updating the alef_version pin", config_path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
use tracing_test::traced_test;
fn workspace_with_version(v: Option<&str>) -> WorkspaceConfig {
let mut toml = String::new();
if let Some(version) = v {
toml.push_str(&format!("alef_version = \"{version}\"\n"));
}
toml::from_str(&toml).expect("valid workspace config")
}
#[test]
fn missing_pin_is_compatible() {
let ws = workspace_with_version(None);
assert!(check_alef_toml_version(&ws).is_ok());
}
#[test]
fn pin_equal_to_cli_passes() {
let ws = workspace_with_version(Some(cli_version()));
assert!(check_alef_toml_version(&ws).is_ok());
}
#[test]
#[traced_test]
fn pin_lower_than_cli_reports_that_generation_preserves_pin() {
let ws = workspace_with_version(Some("0.0.1"));
assert!(check_alef_toml_version(&ws).is_ok());
assert!(logs_contain("generation will not change the pin"));
}
#[test]
fn pin_higher_than_cli_reports_and_does_not_error() {
let ws = workspace_with_version(Some("999.0.0"));
assert!(
check_alef_toml_version(&ws).is_ok(),
"a downgrade must warn, not hard-error"
);
}
#[test]
fn pin_invalid_semver_warns_not_errors() {
let ws = workspace_with_version(Some("not-a-version"));
assert!(
check_alef_toml_version(&ws).is_ok(),
"an unparseable pin must warn and continue, not error"
);
}
#[test]
fn version_check_does_not_rewrite_external_pin() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("alef.toml");
let config = "languages = []\nalef_version = \"0.0.1\"\n";
std::fs::write(&path, config).expect("write fixture");
let workspace: WorkspaceConfig = toml::from_str(config).expect("parse fixture");
check_alef_toml_version(&workspace).expect("check pin");
assert_eq!(std::fs::read_to_string(path).expect("read fixture"), config);
}
fn write_fixture_alef_toml(dir: &std::path::Path, pin: &str) -> std::path::PathBuf {
let path = dir.join("alef.toml");
let config = format!("[workspace]\nalef_version = \"{pin}\"\nlanguages = []\n");
std::fs::write(&path, config).expect("write fixture");
path
}
#[test]
fn auto_update_pin_does_not_write_from_a_dirty_build() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_fixture_alef_toml(dir.path(), "0.0.1");
let workspace = workspace_with_version(Some("0.0.1"));
let before = std::fs::read_to_string(&path).expect("read fixture");
let wrote =
maybe_update_alef_toml_version_pin(&workspace, &path, true, false).expect("dirty build must not error");
assert!(!wrote, "a dirty build must report no write");
assert_eq!(
std::fs::read_to_string(&path).expect("read fixture"),
before,
"a dirty build must never rewrite the pin"
);
}
#[test]
fn auto_update_pin_does_not_write_when_not_opted_in() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_fixture_alef_toml(dir.path(), "0.0.1");
let workspace = workspace_with_version(Some("0.0.1"));
let before = std::fs::read_to_string(&path).expect("read fixture");
let wrote = maybe_update_alef_toml_version_pin(&workspace, &path, false, true).expect("opt-out must not error");
assert!(!wrote, "no opt-in must report no write");
assert_eq!(std::fs::read_to_string(&path).expect("read fixture"), before);
}
#[test]
fn auto_update_pin_writes_when_opted_in_clean_and_newer() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_fixture_alef_toml(dir.path(), "0.0.1");
let workspace = workspace_with_version(Some("0.0.1"));
let wrote =
maybe_update_alef_toml_version_pin(&workspace, &path, true, true).expect("an eligible bump must not error");
assert!(wrote, "an eligible bump must report that it wrote");
let after = std::fs::read_to_string(&path).expect("read fixture");
assert!(
after.contains(&format!("alef_version = \"{}\"", cli_version())),
"pin must be rewritten to the running CLI version:\n{after}"
);
assert!(
after.contains("languages = []"),
"unrelated keys must survive the surgical edit:\n{after}"
);
}
#[test]
fn auto_update_pin_does_not_write_when_pin_is_not_older_than_cli() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_fixture_alef_toml(dir.path(), cli_version());
let workspace = workspace_with_version(Some(cli_version()));
let before = std::fs::read_to_string(&path).expect("read fixture");
let wrote =
maybe_update_alef_toml_version_pin(&workspace, &path, true, true).expect("equal pin must not error");
assert!(!wrote, "an equal pin must not be rewritten");
assert_eq!(std::fs::read_to_string(&path).expect("read fixture"), before);
}
#[test]
fn auto_update_pin_does_not_write_for_invalid_semver() {
let dir = tempfile::tempdir().expect("tempdir");
let path = write_fixture_alef_toml(dir.path(), "not-a-version");
let workspace = workspace_with_version(Some("not-a-version"));
let before = std::fs::read_to_string(&path).expect("read fixture");
let wrote =
maybe_update_alef_toml_version_pin(&workspace, &path, true, true).expect("invalid semver must not error");
assert!(!wrote);
assert_eq!(std::fs::read_to_string(&path).expect("read fixture"), before);
}
#[test]
fn auto_update_pin_does_not_write_when_pin_missing() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("alef.toml");
std::fs::write(&path, "languages = []\n").expect("write fixture");
let workspace = workspace_with_version(None);
let wrote =
maybe_update_alef_toml_version_pin(&workspace, &path, true, true).expect("missing pin must not error");
assert!(!wrote);
}
}