use crate::cli::auto_commit::{commit_paths, print_skip, AutoCommitRequest, AutoCommitResult};
use crate::commands::cli_session::{
post_version_activity, CliVersionActivityPayload,
};
#[cfg(feature = "openapi-gen")]
use crate::commands::{generate_openapi, GenerateOpenApiArgs};
use crate::config::{
load_package_name_files_registry, load_versioning_files_registry,
};
use crate::utils::find_xbp_config_upwards;
use semver::Version;
use std::collections::BTreeSet;
use std::env;
use std::path::{Path, PathBuf};
#[cfg(feature = "openapi-gen")]
use xbp_openapi_gen::cache::{cache_key as openapi_cache_key, source_metadata};
mod adapters;
mod change_guard;
mod path_utils;
pub(crate) use change_guard::*;
pub(crate) use path_utils::*;
mod change_selection;
mod git_ops;
mod registry_paths;
mod report;
mod release_workflow;
pub use release_workflow::run_version_release_command;
pub(crate) use release_workflow::*;
mod scope;
pub(crate) use git_ops::*;
pub(crate) use registry_paths::*;
pub(crate) use report::*;
pub(crate) use scope::*;
mod types;
pub(crate) use change_selection::*;
pub use types::{ReleaseLatestPolicy, VersionReleaseOptions};
pub(crate) use types::*;
mod bump;
mod cargo_dist;
mod discover_services;
mod domain;
mod github_release;
mod release_docs;
mod release_ledger;
#[cfg(feature = "linear")]
mod release_linear;
mod release_notes;
mod workspace_release;
pub(crate) use adapters::*;
pub use bump::run_version_bump_command;
pub use discover_services::run_version_discover_services;
pub use domain::{
check_domain_for_cloudflare_release, run_version_domain_command, VersionDomainCommand,
VersionDomainCommandOptions, VersionDomainDiagnoseOptions, VersionDomainDoctorOptions,
VersionDomainInitOptions, VersionDomainReleaseOptions, VersionDomainSyncOptions,
};
#[cfg(not(feature = "linear"))]
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
struct PublishedLinearInitiative {
id: String,
name: String,
url: Option<String>,
}
pub(crate) use workspace_release::{
heal_publish_surface_for_release, resolve_manifest_workspace_publish,
ManifestWorkspacePublishResolution,
};
pub use workspace_release::{
run_version_workspace_command, WorkspacePublishHealOptions, WorkspacePublishPlanOptions,
WorkspacePublishRunOptions, WorkspaceVersionCheckOptions, WorkspaceVersionCommand,
WorkspaceVersionCommandOptions, WorkspaceVersionSyncOptions, WorkspaceVersionValidateOptions,
};
pub(crate) const LEDGER_STEP_SYNC_VERSION: &str = "sync_version";
pub(crate) const LEDGER_STEP_WORKSPACE_SYNC: &str = "workspace_sync";
pub(crate) const LEDGER_STEP_PUBLISH_PACKAGES: &str = "publish_packages";
pub(crate) const LEDGER_STEP_PUSH_TAG: &str = "push_tag";
pub(crate) const LEDGER_STEP_CREATE_GITHUB_RELEASE: &str = "github_release";
pub(crate) const LEDGER_STEP_CARGO_DIST_BUILD: &str = "cargo_dist_build";
pub(crate) const LEDGER_STEP_UPLOAD_DIST_ASSETS: &str = "upload_dist_assets";
pub(crate) const LEDGER_STEP_UPLOAD_OPENAPI_ASSETS: &str = "upload_openapi_assets";
pub(crate) const LEDGER_STEP_PUBLISH_LINEAR: &str = "publish_linear";
pub(crate) const LEDGER_STEP_SYNC_RELEASE_DOCS: &str = "sync_release_docs";
pub(crate) const LEDGER_STEP_COMMIT_RELEASE_DOCS: &str = "commit_release_docs";
#[cfg(feature = "openapi-gen")]
pub(crate) const LEDGER_STEP_GENERATE_OPENAPI: &str = "generate_openapi";
#[derive(Clone, Debug)]
pub(crate) enum VersionScope {
Repository,
Crate {
crate_root: PathBuf,
crate_relative_root: String,
package_name: String,
tag_prefix: String,
},
Service {
service_root: PathBuf,
service_relative_root: String,
service_name: String,
tag_prefix: String,
cargo_package_name: Option<String>,
version_targets: Vec<String>,
watch_paths: Vec<String>,
},
}
impl VersionReport {
fn highest_worktree(&self) -> Option<Version> {
self.worktree
.iter()
.map(|entry| entry.version.clone())
.max()
}
fn highest_head(&self) -> Option<Version> {
self.head.iter().map(|entry| entry.version.clone()).max()
}
fn highest_local_tag(&self) -> Option<Version> {
self.local_tags
.iter()
.map(|entry| entry.version.clone())
.max()
}
fn highest_remote_tag(&self) -> Option<Version> {
self.remote_tags
.iter()
.map(|entry| entry.version.clone())
.max()
}
fn highest_git(&self) -> Option<Version> {
self.highest_remote_tag()
.or_else(|| self.highest_local_tag())
}
fn highest_registry(&self) -> Option<Version> {
self.registry_versions
.iter()
.filter_map(|entry| entry.latest.clone())
.max()
}
fn highest_available(&self) -> Version {
self.highest_worktree()
.into_iter()
.chain(self.highest_head())
.chain(self.highest_git())
.chain(self.highest_registry())
.max()
.unwrap_or_else(default_version)
}
fn highest_project_local_available(&self) -> Version {
self.highest_worktree()
.into_iter()
.chain(self.highest_registry())
.max()
.unwrap_or_else(default_version)
}
fn divergent_versions(&self) -> Vec<Version> {
let mut versions = BTreeSet::new();
for entry in &self.worktree {
versions.insert(entry.version.clone());
}
for entry in &self.head {
versions.insert(entry.version.clone());
}
for entry in &self.local_tags {
versions.insert(entry.version.clone());
}
for entry in &self.remote_tags {
versions.insert(entry.version.clone());
}
for entry in &self.registry_versions {
if let Some(version) = &entry.latest {
versions.insert(version.clone());
}
}
versions.into_iter().collect()
}
}
pub async fn run_version_command(
target: Option<String>,
git_only: bool,
_debug: bool,
) -> Result<(), String> {
if git_only && target.is_some() {
return Err("`xbp version --git` does not accept `major`, `minor`, `patch`, or explicit version values.".to_string());
}
let invocation_dir: PathBuf = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
let project_root: PathBuf = resolve_project_root();
let version_scope: VersionScope =
resolve_version_scope_with_prompt(&project_root, &invocation_dir)?;
let registry: Vec<String> = load_versioning_files_registry()?;
if git_only {
print_git_versions(&project_root, &version_scope)?;
return Ok(());
}
match target.as_deref() {
None => {
let mut report: VersionReport =
collect_version_report(&project_root, &invocation_dir, ®istry, &version_scope);
match load_package_name_files_registry() {
Ok(lookups) => {
report.registry_versions = collect_registry_versions(
&project_root,
&invocation_dir,
&lookups,
&version_scope,
&mut report.warnings,
)
.await;
}
Err(err) => report.warnings.push(err),
}
print_version_report(&project_root, &report);
Ok(())
}
Some(bump_target @ ("major" | "minor" | "patch")) => {
enforce_version_change_guard(&project_root, Some(&version_scope))?;
let repo = project_root
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("repository")
.to_string();
let selection = resolve_changed_target_selection(
&project_root,
&invocation_dir,
®istry,
&version_scope,
&repo,
)?;
let current: Version = read_highest_version_from_targets(&selection.version_targets)?
.unwrap_or_else(|| {
resolve_current_version_for_bump(
&project_root,
&invocation_dir,
®istry,
&version_scope,
)
});
let next: Version = bump_version(¤t, bump_target);
let updated_paths =
write_version_to_selected_paths(&selection.version_targets, &next, false)?;
let updated = updated_paths.len();
println!(
"Updated {} version file(s) from {} to {}.",
updated, current, next
);
auto_commit_command_paths(
&project_root,
updated_paths,
format!("chore(version): update version to {}", next),
"xbp version",
)
.await;
record_version_change_guard(&project_root, Some(&version_scope))?;
sync_cli_version_write_activity(
&project_root,
&version_scope,
&next,
format!(
"Updated {} version file(s) from {} to {}.",
updated, current, next
),
)
.await;
Ok(())
}
Some(explicit) => {
enforce_version_change_guard(&project_root, Some(&version_scope))?;
let repo = project_root
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("repository")
.to_string();
let selection = resolve_changed_target_selection(
&project_root,
&invocation_dir,
®istry,
&version_scope,
&repo,
)?;
if let Some((package_name, version)) = parse_package_version_target(explicit)? {
let updated_paths = write_package_version_to_configured_files_with_paths(
&project_root,
&invocation_dir,
®istry,
&version_scope,
&package_name,
&version,
)?;
let updated = updated_paths.len();
println!(
"Updated {} file(s) for package `{}` to {}.",
updated, package_name, version
);
auto_commit_command_paths(
&project_root,
updated_paths,
format!("chore(version): set {} to {}", package_name, version),
"xbp version",
)
.await;
record_version_change_guard(&project_root, Some(&version_scope))?;
sync_cli_version_write_activity(
&project_root,
&version_scope,
&version,
format!(
"Updated {} file(s) for package `{}` to {}.",
updated, package_name, version
),
)
.await;
} else {
let version: Version = parse_version(explicit)?;
let updated_paths =
write_version_to_selected_paths(&selection.version_targets, &version, false)?;
let updated = updated_paths.len();
println!("Updated {} version file(s) to {}.", updated, version);
auto_commit_command_paths(
&project_root,
updated_paths,
format!("chore(version): update version to {}", version),
"xbp version",
)
.await;
record_version_change_guard(&project_root, Some(&version_scope))?;
sync_cli_version_write_activity(
&project_root,
&version_scope,
&version,
format!("Updated {} version file(s) to {}.", updated, version),
)
.await;
}
Ok(())
}
}
}
pub async fn print_version() {
crate::cli::help_render::emit_version_info(env!("CARGO_PKG_VERSION"));
}
fn resolve_project_root() -> PathBuf {
let cwd: PathBuf = env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
resolve_project_root_from(&cwd)
}
fn resolve_project_root_from(cwd: &Path) -> PathBuf {
let root = if let Some(found) = find_xbp_config_upwards(cwd) {
found.project_root
} else if let Some(root) = git_repository_root(cwd) {
root
} else {
cwd.to_path_buf()
};
crate::utils::canonicalize_for_subprocess(&root)
}
pub(crate) async fn auto_commit_command_paths(
project_root: &Path,
paths: Vec<PathBuf>,
message: String,
action_label: &'static str,
) {
let _ =
auto_commit_command_paths_result(project_root, paths, message, action_label, false).await;
}
pub(crate) async fn auto_commit_command_paths_result(
project_root: &Path,
paths: Vec<PathBuf>,
message: String,
action_label: &'static str,
push: bool,
) -> Result<AutoCommitResult, String> {
match commit_paths(AutoCommitRequest {
project_root,
paths,
message,
action_label,
push,
})
.await
{
Ok(AutoCommitResult::Committed(outcome)) => Ok(AutoCommitResult::Committed(outcome)),
Ok(AutoCommitResult::Skipped(reason)) => {
print_skip(action_label, &reason);
Ok(AutoCommitResult::Skipped(reason))
}
Err(e) => {
print_skip(action_label, &e);
Err(e)
}
}
}
pub(crate) async fn sync_cli_version_write_activity(
project_root: &Path,
version_scope: &VersionScope,
version: &Version,
message: String,
) {
let (repository_owner, repository_name) = resolve_optional_github_repository(project_root);
let scope_label = version_scope_label(
version_scope,
repository_name.as_deref().unwrap_or_else(|| {
project_root
.file_name()
.and_then(|value| value.to_str())
.unwrap_or("repository")
}),
);
let payload = CliVersionActivityPayload {
command_kind: "version".to_string(),
repository_owner,
repository_name,
scope_kind: version_scope_kind(version_scope).to_string(),
scope_label,
version: version.to_string(),
tag_name: None,
title: None,
release_url: None,
message_markdown: Some(message),
published_initiatives: Vec::new(),
};
if let Err(error) = post_version_activity(&payload).await {
eprintln!("Warning: {}", error);
}
}
#[cfg(test)]
mod tests;