use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::aur_source::AurSourceConfig;
use super::hooks::HookEntry;
use super::string_or_bool::HumanDuration;
use super::{StringOrBool, deserialize_string_or_bool_opt};
mod homebrew;
pub use homebrew::*;
mod chocolatey;
pub use chocolatey::*;
mod winget;
pub use winget::*;
mod aur;
pub use aur::*;
mod krew;
pub use krew::*;
mod nix;
pub use nix::*;
mod schemastore;
pub use schemastore::{SchemaEntry, SchemaMode, SchemastoreConfig};
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct RepositoryConfig {
pub owner: Option<String>,
pub name: Option<String>,
pub token: Option<String>,
pub token_type: Option<String>,
pub branch: Option<String>,
pub git: Option<GitRepoConfig>,
pub pull_request: Option<PullRequestConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct GitRepoConfig {
pub url: Option<String>,
pub ssh_command: Option<String>,
pub private_key: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct PullRequestConfig {
pub enabled: Option<bool>,
pub draft: Option<bool>,
pub body: Option<String>,
pub base: Option<PullRequestBaseConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct PullRequestBaseConfig {
pub owner: Option<String>,
pub name: Option<String>,
pub branch: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct CommitAuthorConfig {
pub name: Option<String>,
pub email: Option<String>,
pub signing: Option<CommitSigningConfig>,
#[serde(default)]
pub use_github_app_token: bool,
}
impl CommitAuthorConfig {
pub fn normalize_defaults(&mut self) {
if self.name.as_deref().is_none_or(str::is_empty) {
self.name = Some("anodizer".to_string());
}
if self.email.as_deref().is_none_or(str::is_empty) {
self.email = Some("bot@anodizer.dev".to_string());
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct CommitSigningConfig {
pub enabled: Option<bool>,
pub key: Option<String>,
pub program: Option<String>,
pub format: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct PublishConfig {
pub cargo: Option<CargoPublishConfig>,
pub homebrew: Option<HomebrewConfig>,
pub homebrew_cask: Option<HomebrewCaskConfig>,
pub scoop: Option<ScoopConfig>,
pub chocolatey: Option<ChocolateyConfig>,
pub winget: Option<WingetConfig>,
pub aur: Option<AurConfig>,
pub aur_source: Option<AurSourceConfig>,
pub krew: Option<KrewConfig>,
pub nix: Option<NixConfig>,
pub on_error: Option<Vec<HookEntry>>,
pub on_rollback: Option<Vec<HookEntry>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct CargoPublishConfig {
pub registry: Option<String>,
pub index: Option<String>,
pub index_timeout: Option<u64>,
pub wait_for_workspace_deps: Option<WaitForWorkspaceDepsConfig>,
pub no_verify: Option<bool>,
pub allow_dirty: Option<bool>,
pub features: Option<Vec<String>>,
pub all_features: Option<bool>,
pub no_default_features: Option<bool>,
pub target: Option<String>,
pub target_dir: Option<PathBuf>,
pub jobs: Option<u32>,
pub keep_going: Option<bool>,
pub manifest_path: Option<PathBuf>,
pub locked: Option<bool>,
pub offline: Option<bool>,
pub frozen: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth: Option<CargoAuthMode>,
#[serde(default, deserialize_with = "deserialize_string_or_bool_opt")]
pub skip: Option<StringOrBool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
#[serde(rename = "if")]
pub if_condition: Option<String>,
pub retain_on_rollback: Option<bool>,
}
impl CargoPublishConfig {
pub fn resolved_auth(&self) -> CargoAuthMode {
self.auth.unwrap_or_default()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "kebab-case")]
pub enum CargoAuthMode {
#[default]
Auto,
Token,
Oidc,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct WaitForWorkspaceDepsConfig {
pub enabled: Option<bool>,
pub poll_interval: Option<HumanDuration>,
pub max_wait: Option<HumanDuration>,
}
impl WaitForWorkspaceDepsConfig {
pub const DEFAULT_POLL_INTERVAL: std::time::Duration = std::time::Duration::from_secs(5);
pub const DEFAULT_MAX_WAIT: std::time::Duration = std::time::Duration::from_secs(300);
pub fn resolved_enabled(&self) -> bool {
self.enabled.unwrap_or(false)
}
pub fn resolved_poll_interval(&self) -> std::time::Duration {
self.poll_interval
.map(|d| d.duration())
.unwrap_or(Self::DEFAULT_POLL_INTERVAL)
}
pub fn resolved_max_wait(&self) -> std::time::Duration {
self.max_wait
.map(|d| d.duration())
.unwrap_or(Self::DEFAULT_MAX_WAIT)
}
}
pub trait PublisherGateOverrides {
fn required_override(&self) -> Option<bool>;
fn retain_on_rollback_override(&self) -> Option<bool>;
}
macro_rules! impl_publisher_gate_overrides {
($($t:ty),+ $(,)?) => {
$(impl PublisherGateOverrides for $t {
fn required_override(&self) -> Option<bool> {
self.required
}
fn retain_on_rollback_override(&self) -> Option<bool> {
self.retain_on_rollback
}
})+
};
}
impl_publisher_gate_overrides!(
CargoPublishConfig,
HomebrewConfig,
HomebrewCaskConfig,
ScoopConfig,
ChocolateyConfig,
WingetConfig,
AurConfig,
AurSourceConfig,
KrewConfig,
NixConfig,
super::ReleaseConfig,
super::DockerHubConfig,
super::ArtifactoryConfig,
super::UploadConfig,
super::CloudSmithConfig,
super::NpmConfig,
super::GemFuryConfig,
super::PypiConfig,
super::HomebrewCoreConfig,
);
#[cfg(test)]
mod tests {
use super::*;
use std::time::Duration;
#[test]
fn normalize_defaults_fills_missing_name_and_email() {
let mut c = CommitAuthorConfig::default();
c.normalize_defaults();
assert_eq!(c.name.as_deref(), Some("anodizer"));
assert_eq!(c.email.as_deref(), Some("bot@anodizer.dev"));
}
#[test]
fn normalize_defaults_treats_empty_string_as_missing() {
let mut c = CommitAuthorConfig {
name: Some(String::new()),
email: Some(String::new()),
..Default::default()
};
c.normalize_defaults();
assert_eq!(c.name.as_deref(), Some("anodizer"));
assert_eq!(c.email.as_deref(), Some("bot@anodizer.dev"));
}
#[test]
fn normalize_defaults_preserves_user_supplied_identity() {
let mut c = CommitAuthorConfig {
name: Some("Release Bot".into()),
email: Some("release@corp.example".into()),
..Default::default()
};
c.normalize_defaults();
assert_eq!(c.name.as_deref(), Some("Release Bot"));
assert_eq!(c.email.as_deref(), Some("release@corp.example"));
}
#[test]
fn cargo_auth_mode_defaults_to_auto() {
assert_eq!(CargoAuthMode::default(), CargoAuthMode::Auto);
assert_eq!(
CargoPublishConfig::default().resolved_auth(),
CargoAuthMode::Auto
);
}
#[test]
fn cargo_auth_mode_deserializes_kebab_case() {
assert_eq!(
serde_yaml_ng::from_str::<CargoAuthMode>("auto").unwrap(),
CargoAuthMode::Auto
);
assert_eq!(
serde_yaml_ng::from_str::<CargoAuthMode>("token").unwrap(),
CargoAuthMode::Token
);
assert_eq!(
serde_yaml_ng::from_str::<CargoAuthMode>("oidc").unwrap(),
CargoAuthMode::Oidc
);
assert!(serde_yaml_ng::from_str::<CargoAuthMode>("bogus").is_err());
}
#[test]
fn resolved_auth_returns_explicit_value() {
let cfg = CargoPublishConfig {
auth: Some(CargoAuthMode::Oidc),
..Default::default()
};
assert_eq!(cfg.resolved_auth(), CargoAuthMode::Oidc);
}
#[test]
fn wait_for_workspace_deps_defaults() {
let d = WaitForWorkspaceDepsConfig::default();
assert!(!d.resolved_enabled());
assert_eq!(d.resolved_poll_interval(), Duration::from_secs(5));
assert_eq!(d.resolved_max_wait(), Duration::from_secs(300));
}
#[test]
fn wait_for_workspace_deps_user_values_win() {
let cfg: WaitForWorkspaceDepsConfig =
serde_yaml_ng::from_str("enabled: true\npoll_interval: 10s\nmax_wait: 2m").unwrap();
assert!(cfg.resolved_enabled());
assert_eq!(cfg.resolved_poll_interval(), Duration::from_secs(10));
assert_eq!(cfg.resolved_max_wait(), Duration::from_secs(120));
}
#[test]
fn gate_overrides_expose_required_and_retain() {
let cfg = CargoPublishConfig {
required: Some(false),
retain_on_rollback: Some(true),
..Default::default()
};
assert_eq!(cfg.required_override(), Some(false));
assert_eq!(cfg.retain_on_rollback_override(), Some(true));
let bare = CargoPublishConfig::default();
assert_eq!(bare.required_override(), None);
assert_eq!(bare.retain_on_rollback_override(), None);
}
}