use std::path::PathBuf;
use std::{
env,
fs,
};
use anyhow::{
Context,
Result,
};
use cargo_plugin_utils::common::get_owner_repo;
use clap::Parser;
use crate::github;
#[derive(Parser, Debug)]
pub struct BuildVersionArgs {
#[arg(long)]
owner: Option<String>,
#[arg(long)]
repo: Option<String>,
#[arg(long, env = "GITHUB_TOKEN")]
github_token: Option<String>,
#[arg(long, default_value = "./Cargo.toml")]
manifest: PathBuf,
#[arg(long, default_value = ".")]
repo_path: PathBuf,
#[arg(long, default_value = "version")]
format: String,
}
#[allow(clippy::disallowed_methods)] pub fn build_version(args: BuildVersionArgs) -> Result<()> {
let env_version = ["BUILD_VERSION", "CARGO_PKG_VERSION_OVERRIDE"]
.into_iter()
.find_map(|key| env::var(key).ok())
.filter(|v| !v.trim().is_empty());
if let Some(version) = env_version {
match args.format.as_str() {
"version" => println!("{}", version),
"json" => println!("{{\"version\":\"{}\",\"source\":\"environment\"}}", version),
_ => anyhow::bail!("Invalid format: {}", args.format),
}
return Ok(());
}
let is_github_actions = env::var("GITHUB_ACTIONS").is_ok();
if is_github_actions {
let (owner, repo) = get_owner_repo(args.owner, args.repo)?;
let github_token = args.github_token.as_deref();
let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
if let Ok((_, next)) =
rt.block_on(github::calculate_next_version(&owner, &repo, github_token))
{
match args.format.as_str() {
"version" => println!("{}", next),
"json" => println!("{{\"version\":\"{}\",\"source\":\"github_api\"}}", next),
_ => anyhow::bail!("Invalid format: {}", args.format),
}
return Ok(());
}
}
if let Some(manifest_version) = read_manifest_version(&args.manifest) {
let trimmed = manifest_version.trim();
if !trimmed.is_empty() && trimmed != "0.0.0" {
let version_with_sha = short_sha(&args.repo_path)
.map(|sha| format!("{trimmed}-{sha}"))
.unwrap_or_else(|| trimmed.to_string());
match args.format.as_str() {
"version" => println!("{version_with_sha}"),
"json" => println!(
"{{\"version\":\"{}\",\"source\":\"cargo_toml\"}}",
version_with_sha
),
_ => anyhow::bail!("Invalid format: {}", args.format),
}
return Ok(());
}
}
let repo = gix::discover(&args.repo_path).with_context(|| {
format!(
"Failed to discover git repository at {}",
args.repo_path.display()
)
})?;
let head = repo.head().context("Failed to read HEAD")?;
let commit_id = head.id().context("HEAD does not point to a commit")?;
let short_sha = commit_id
.shorten()
.context("Failed to shorten commit SHA")?;
let dev_version = format!("0.0.0-dev-{}", short_sha);
match args.format.as_str() {
"version" => println!("{}", dev_version),
"json" => println!(
"{{\"version\":\"{}\",\"sha\":\"{}\",\"source\":\"git\"}}",
dev_version, short_sha
),
_ => anyhow::bail!("Invalid format: {}", args.format),
}
Ok(())
}
pub fn build_version_default() -> Result<()> {
build_version_for_repo(".")
}
pub fn build_version_for_repo(repo_path: impl Into<PathBuf>) -> Result<()> {
let repo_root: PathBuf = repo_path.into();
let manifest = repo_root.join("Cargo.toml");
build_version(BuildVersionArgs {
owner: None,
repo: None,
github_token: None,
manifest,
repo_path: repo_root,
format: "version".to_string(),
})
}
pub fn compute_version_string(repo_path: impl Into<PathBuf>) -> Result<String> {
let repo_root: PathBuf = repo_path.into();
let manifest = repo_root.join("Cargo.toml");
let env_version = ["BUILD_VERSION", "CARGO_PKG_VERSION_OVERRIDE"]
.into_iter()
.find_map(|key| env::var(key).ok())
.filter(|v| !v.trim().is_empty());
if let Some(version) = env_version {
return Ok(version);
}
let is_github_actions = env::var("GITHUB_ACTIONS").is_ok();
if is_github_actions {
let (owner, repo) = get_owner_repo(None, None)?;
let github_token = None::<String>;
let rt = tokio::runtime::Runtime::new().context("Failed to create tokio runtime")?;
if let Ok((_, next)) = rt.block_on(github::calculate_next_version(
&owner,
&repo,
github_token.as_deref(),
)) {
return Ok(next);
}
}
if let Some(manifest_version) = read_manifest_version(&manifest) {
let trimmed = manifest_version.trim();
if !trimmed.is_empty() && trimmed != "0.0.0" {
let version_with_sha = short_sha(&repo_root)
.map(|sha| format!("{trimmed}-{sha}"))
.unwrap_or_else(|| trimmed.to_string());
return Ok(version_with_sha);
}
}
let repo = gix::discover(&repo_root).with_context(|| {
format!(
"Failed to discover git repository at {}",
repo_root.display()
)
})?;
let head = repo.head().context("Failed to read HEAD")?;
let commit_id = head.id().context("HEAD does not point to a commit")?;
let short_sha = commit_id
.shorten()
.context("Failed to shorten commit SHA")?;
Ok(format!("0.0.0-dev-{}", short_sha))
}
fn short_sha(repo_path: &PathBuf) -> Option<String> {
let repo = gix::discover(repo_path).ok()?;
let head = repo.head().ok()?;
let commit_id = head.id()?;
let short = commit_id.shorten().ok()?;
Some(short.to_string())
}
fn read_manifest_version(manifest: &PathBuf) -> Option<String> {
let contents = fs::read_to_string(manifest).ok()?;
let value: toml::Value = toml::from_str(&contents).ok()?;
value
.get("package")
.and_then(|pkg| pkg.get("version"))
.and_then(|v| v.as_str())
.map(ToString::to_string)
}
#[cfg(test)]
mod tests {
use std::env;
use super::*;
#[test]
fn test_build_version_env_priority() {
unsafe {
env::set_var("BUILD_VERSION", "1.2.3");
}
let args = BuildVersionArgs {
owner: None,
repo: None,
github_token: None,
manifest: "./Cargo.toml".into(),
repo_path: ".".into(),
format: "version".to_string(),
};
let result = build_version(args);
unsafe {
env::remove_var("BUILD_VERSION");
}
assert!(result.is_ok());
}
#[test]
fn test_build_version_env_json() {
unsafe {
env::set_var("BUILD_VERSION", "2.0.0");
}
let args = BuildVersionArgs {
owner: None,
repo: None,
github_token: None,
manifest: "./Cargo.toml".into(),
repo_path: ".".into(),
format: "json".to_string(),
};
let result = build_version(args);
unsafe {
env::remove_var("BUILD_VERSION");
}
assert!(result.is_ok());
}
#[test]
fn test_build_version_cargo_pkg_version() {
unsafe {
env::remove_var("BUILD_VERSION");
env::remove_var("CARGO_PKG_VERSION_OVERRIDE");
env::set_var("CARGO_PKG_VERSION", "1.5.0");
}
let args = BuildVersionArgs {
owner: None,
repo: None,
github_token: None,
manifest: "./Cargo.toml".into(),
repo_path: ".".into(),
format: "version".to_string(),
};
let result = build_version(args);
unsafe {
env::remove_var("CARGO_PKG_VERSION");
}
let _ = result;
}
#[test]
fn test_build_version_invalid_format() {
unsafe {
env::set_var("BUILD_VERSION", "1.0.0");
}
let args = BuildVersionArgs {
owner: None,
repo: None,
github_token: None,
manifest: "./Cargo.toml".into(),
repo_path: ".".into(),
format: "invalid".to_string(),
};
let result = build_version(args);
unsafe {
env::remove_var("BUILD_VERSION");
}
assert!(result.is_err());
}
#[test]
fn test_build_version_empty_env_var() {
unsafe {
env::set_var("BUILD_VERSION", "");
}
let args = BuildVersionArgs {
owner: None,
repo: None,
github_token: None,
manifest: "./Cargo.toml".into(),
repo_path: ".".into(),
format: "version".to_string(),
};
let result = build_version(args);
unsafe {
env::remove_var("BUILD_VERSION");
}
let _ = result;
}
#[test]
fn test_build_version_override_priority() {
unsafe {
env::set_var("BUILD_VERSION", "1.0.0");
env::set_var("CARGO_PKG_VERSION_OVERRIDE", "2.0.0");
}
let args = BuildVersionArgs {
owner: None,
repo: None,
github_token: None,
manifest: "./Cargo.toml".into(),
repo_path: ".".into(),
format: "version".to_string(),
};
let result = build_version(args);
unsafe {
env::remove_var("BUILD_VERSION");
env::remove_var("CARGO_PKG_VERSION_OVERRIDE");
}
assert!(result.is_ok());
}
}