use std::collections::{BTreeMap, HashMap};
use std::path::PathBuf;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(untagged)]
pub enum IncludeSpec {
Path(String),
FromFile { from_file: IncludeFilePath },
FromUrl { from_url: IncludeUrlConfig },
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct IncludeFilePath {
pub path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct IncludeUrlConfig {
pub url: String,
pub headers: Option<HashMap<String, String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(default, deny_unknown_fields)]
pub struct Config {
pub version: Option<u32>,
pub project_name: String,
#[serde(default = "default_dist")]
pub dist: PathBuf,
pub includes: Option<Vec<IncludeSpec>>,
pub env_files: Option<EnvFilesConfig>,
pub defaults: Option<Defaults>,
pub before: Option<HooksConfig>,
pub after: Option<HooksConfig>,
pub before_publish: Option<HooksConfig>,
pub crates: Vec<CrateConfig>,
pub changelog: Option<ChangelogConfig>,
#[serde(default, deserialize_with = "deserialize_signs")]
#[schemars(schema_with = "signs_schema")]
pub signs: Vec<SignConfig>,
#[serde(default, deserialize_with = "deserialize_binary_signs")]
#[schemars(schema_with = "signs_schema")]
pub binary_signs: Vec<SignConfig>,
pub docker_signs: Option<Vec<DockerSignConfig>>,
#[serde(default, deserialize_with = "deserialize_upx")]
#[schemars(schema_with = "upx_schema")]
pub upx: Vec<UpxConfig>,
pub snapshot: Option<SnapshotConfig>,
pub nightly: Option<NightlyConfig>,
pub announce: Option<AnnounceConfig>,
pub report_sizes: Option<bool>,
#[serde(default)]
pub env: Option<Vec<String>>,
pub variables: Option<BTreeMap<String, String>>,
pub publishers: Option<Vec<PublisherConfig>>,
pub dockerhub: Option<Vec<DockerHubConfig>>,
pub artifactories: Option<Vec<ArtifactoryConfig>>,
pub cloudsmiths: Option<Vec<CloudSmithConfig>>,
pub homebrew_casks: Option<Vec<HomebrewCaskConfig>>,
pub version_files: Option<Vec<String>>,
pub tag: Option<TagConfig>,
pub git: Option<GitConfig>,
pub partial: Option<PartialConfig>,
pub workspaces: Option<Vec<WorkspaceConfig>>,
pub source: Option<SourceConfig>,
#[serde(default, deserialize_with = "deserialize_sboms")]
#[schemars(schema_with = "sboms_schema")]
pub sboms: Vec<SbomConfig>,
pub attestations: Option<AttestationConfig>,
pub release: Option<ReleaseConfig>,
pub github_urls: Option<GitHubUrlsConfig>,
pub gitlab_urls: Option<GitLabUrlsConfig>,
pub gitea_urls: Option<GiteaUrlsConfig>,
pub force_token: Option<ForceTokenKind>,
pub notarize: Option<NotarizeConfig>,
pub metadata: Option<MetadataConfig>,
pub template_files: Option<Vec<TemplateFileConfig>>,
pub monorepo: Option<MonorepoConfig>,
#[serde(default, deserialize_with = "deserialize_makeselfs")]
#[schemars(schema_with = "makeselfs_schema")]
pub makeselfs: Vec<MakeselfConfig>,
#[serde(default, deserialize_with = "deserialize_appimages")]
#[schemars(schema_with = "appimages_schema")]
pub appimages: Vec<AppImageConfig>,
#[serde(default)]
pub verify_release: VerifyReleaseConfig,
#[serde(alias = "srpm")]
pub srpms: Option<SrpmConfig>,
pub milestones: Option<Vec<MilestoneConfig>>,
pub uploads: Option<Vec<UploadConfig>>,
pub aur_sources: Option<Vec<AurSourceConfig>>,
pub retry: Option<RetryConfig>,
#[serde(default)]
pub mcp: McpConfig,
#[serde(default)]
pub schemastore: crate::config::publishers::SchemastoreConfig,
pub npms: Option<Vec<NpmConfig>>,
#[serde(alias = "furies")]
pub gemfury: Option<Vec<GemFuryConfig>>,
#[serde(skip)]
#[schemars(skip)]
pub derived_metadata: BTreeMap<String, MetadataConfig>,
}
fn signs_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let mut schema = generator.subschema_for::<Vec<SignConfig>>();
schema.ensure_object().insert(
"description".to_owned(),
"Artifact signing configurations (cosign, GPG, etc.). Accepts a single object or array."
.into(),
);
schema
}
fn upx_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let mut schema = generator.subschema_for::<Vec<UpxConfig>>();
schema.ensure_object().insert(
"description".to_owned(),
"UPX binary compression configurations. Accepts a single object or array.".into(),
);
schema
}
fn sboms_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
let mut schema = generator.subschema_for::<Vec<SbomConfig>>();
schema.ensure_object().insert(
"description".to_owned(),
"SBOM generation configurations. Accepts a single object or array.".into(),
);
schema
}
fn default_dist() -> PathBuf {
PathBuf::from("./dist")
}
impl Default for Config {
fn default() -> Self {
Config {
version: None,
project_name: String::new(),
dist: default_dist(),
includes: None,
env_files: None,
defaults: None,
before: None,
after: None,
before_publish: None,
crates: Vec::new(),
changelog: None,
signs: Vec::new(),
binary_signs: Vec::new(),
docker_signs: None,
upx: Vec::new(),
snapshot: None,
nightly: None,
announce: None,
report_sizes: None,
env: None,
variables: None,
publishers: None,
dockerhub: None,
artifactories: None,
cloudsmiths: None,
homebrew_casks: None,
version_files: None,
tag: None,
git: None,
partial: None,
workspaces: None,
source: None,
sboms: Vec::new(),
attestations: None,
release: None,
github_urls: None,
gitlab_urls: None,
gitea_urls: None,
force_token: None,
notarize: None,
metadata: None,
template_files: None,
monorepo: None,
makeselfs: Vec::new(),
appimages: Vec::new(),
verify_release: VerifyReleaseConfig::default(),
srpms: None,
milestones: None,
uploads: None,
aur_sources: None,
retry: None,
mcp: McpConfig::default(),
schemastore: crate::config::publishers::SchemastoreConfig::default(),
npms: None,
gemfury: None,
derived_metadata: BTreeMap::new(),
}
}
}
impl Config {
pub fn crate_universe(&self) -> Vec<&CrateConfig> {
self.crate_universe_walk().0
}
pub fn find_crate(&self, name: &str) -> Option<&CrateConfig> {
self.crate_universe().into_iter().find(|c| c.name == name)
}
pub fn crate_universe_collision_warnings(&self) -> Vec<String> {
self.crate_universe_walk().1
}
fn crate_universe_walk(&self) -> (Vec<&CrateConfig>, Vec<String>) {
let mut out: Vec<&CrateConfig> = self.crates.iter().collect();
let mut warnings = Vec::new();
for ws in self.workspaces.iter().flatten() {
for c in &ws.crates {
if let Some(existing) = out.iter().find(|e| e.name == c.name) {
if existing.path != c.path {
warnings.push(format!(
"workspace '{}' crate '{}' path '{}' shadowed by \
prior entry with path '{}'; workspace entry dropped (name \
collision with different paths — likely a config mistake)",
ws.name, c.name, c.path, existing.path
));
}
continue;
}
out.push(c);
}
}
(out, warnings)
}
pub fn monorepo_tag_prefix(&self) -> Option<&str> {
self.monorepo.as_ref().and_then(|m| m.tag_prefix.as_deref())
}
pub fn monorepo_dir(&self) -> Option<&str> {
self.monorepo.as_ref().and_then(|m| m.dir.as_deref())
}
pub fn effective_default_targets(&self) -> Vec<String> {
self.defaults
.as_ref()
.and_then(|d| d.targets.clone())
.filter(|t| !t.is_empty())
.unwrap_or_else(|| {
crate::target::DEFAULT_TARGETS
.iter()
.map(|s| (*s).to_string())
.collect()
})
}
pub fn default_cross_strategy(&self) -> CrossStrategy {
self.defaults
.as_ref()
.and_then(|d| d.cross.clone())
.unwrap_or(CrossStrategy::Auto)
}
fn derived_for(&self, crate_name: &str) -> Option<&MetadataConfig> {
self.derived_metadata.get(crate_name)
}
pub fn primary_crate_name(&self) -> Option<&str> {
self.crate_universe().first().map(|c| c.name.as_str())
}
pub fn meta_homepage_project(&self) -> Option<&str> {
self.meta_homepage()
.or_else(|| self.meta_homepage_for(self.primary_crate_name()?))
}
pub fn meta_description_project(&self) -> Option<&str> {
self.meta_description()
.or_else(|| self.meta_description_for(self.primary_crate_name()?))
}
pub fn meta_repository_project(&self) -> Option<&str> {
self.meta_repository()
.or_else(|| self.meta_repository_for(self.primary_crate_name()?))
}
pub fn meta_license_project(&self) -> Option<&str> {
self.meta_license()
.or_else(|| self.meta_license_for(self.primary_crate_name()?))
}
pub fn meta_documentation_project(&self) -> Option<&str> {
self.meta_documentation()
.or_else(|| self.meta_documentation_for(self.primary_crate_name()?))
}
pub fn meta_homepage(&self) -> Option<&str> {
self.metadata.as_ref().and_then(|m| m.homepage.as_deref())
}
pub fn meta_license(&self) -> Option<&str> {
self.metadata.as_ref().and_then(|m| m.license.as_deref())
}
pub fn meta_repository(&self) -> Option<&str> {
self.metadata.as_ref().and_then(|m| m.repository.as_deref())
}
pub fn meta_description(&self) -> Option<&str> {
self.metadata
.as_ref()
.and_then(|m| m.description.as_deref())
}
pub fn meta_documentation(&self) -> Option<&str> {
self.metadata
.as_ref()
.and_then(|m| m.documentation.as_deref())
}
pub fn meta_maintainers(&self) -> &[String] {
self.metadata
.as_ref()
.and_then(|m| m.maintainers.as_deref())
.unwrap_or(&[])
}
pub fn meta_first_maintainer(&self) -> Option<&str> {
self.meta_maintainers().first().map(|s| s.as_str())
}
pub fn meta_homepage_for(&self, crate_name: &str) -> Option<&str> {
self.meta_homepage()
.or_else(|| self.derived_for(crate_name)?.homepage.as_deref())
}
pub fn meta_license_for(&self, crate_name: &str) -> Option<&str> {
self.meta_license()
.or_else(|| self.derived_for(crate_name)?.license.as_deref())
}
pub fn meta_repository_for(&self, crate_name: &str) -> Option<&str> {
self.meta_repository()
.or_else(|| self.derived_for(crate_name)?.repository.as_deref())
}
pub fn meta_description_for(&self, crate_name: &str) -> Option<&str> {
self.meta_description()
.or_else(|| self.derived_for(crate_name)?.description.as_deref())
}
pub fn meta_documentation_for(&self, crate_name: &str) -> Option<&str> {
self.meta_documentation()
.or_else(|| self.derived_for(crate_name)?.documentation.as_deref())
}
pub fn meta_maintainers_for(&self, crate_name: &str) -> &[String] {
let top = self.meta_maintainers();
if !top.is_empty() {
return top;
}
self.derived_for(crate_name)
.and_then(|m| m.maintainers.as_deref())
.unwrap_or(&[])
}
pub fn meta_first_maintainer_for(&self, crate_name: &str) -> Option<&str> {
self.meta_maintainers_for(crate_name)
.first()
.map(|s| s.as_str())
}
pub fn meta_vendor_for(&self, crate_name: &str) -> Option<String> {
self.meta_first_maintainer_for(crate_name)
.and_then(maintainer_name_only)
}
pub fn populate_derived_metadata(&mut self, base_dir: &std::path::Path) {
let crate_paths: Vec<(String, String)> = self
.crate_universe()
.into_iter()
.map(|c| (c.name.clone(), c.path.clone()))
.collect();
for (name, path) in crate_paths {
let crate_dir = base_dir.join(&path);
let derived = derive_metadata_from_cargo_toml(&crate_dir);
self.derived_metadata.insert(name, derived);
}
}
pub fn has_gpg_sign_configured(&self) -> bool {
let top_level = self
.signs
.iter()
.chain(self.binary_signs.iter())
.any(|s| s.is_gpg());
if top_level {
return true;
}
self.workspaces.iter().flatten().any(|w| {
w.signs
.iter()
.chain(w.binary_signs.iter())
.any(|s| s.is_gpg())
})
}
}
#[must_use]
pub fn config_schema() -> serde_json::Value {
let schema = schemars::generate::SchemaSettings::draft07()
.into_generator()
.into_root_schema_for::<Config>();
let mut value = schema.to_value();
canonicalize_schema(&mut value);
value
}
const SCHEMA_KEYWORD_ORDER: &[&str] = &[
"$id",
"$schema",
"title",
"description",
"default",
"deprecated",
"readOnly",
"writeOnly",
"type",
"format",
"enum",
"const",
"allOf",
"anyOf",
"oneOf",
"not",
"if",
"then",
"else",
"multipleOf",
"maximum",
"exclusiveMaximum",
"minimum",
"exclusiveMinimum",
"maxLength",
"minLength",
"pattern",
"items",
"additionalItems",
"maxItems",
"minItems",
"uniqueItems",
"contains",
"maxProperties",
"minProperties",
"required",
"properties",
"patternProperties",
"additionalProperties",
"propertyNames",
"$ref",
"definitions",
];
const SCHEMA_DEFINITION_MAPS: &[&str] = &["properties", "patternProperties", "definitions"];
fn canonicalize_schema(value: &mut serde_json::Value) {
use serde_json::Value;
match value {
Value::Object(map) => {
if let Some(Value::String(d)) = map.get_mut("description") {
*d = collapse_description(d);
}
if let Some(Value::Array(required)) = map.get_mut("required") {
required.sort_by(|a, b| match (a.as_str(), b.as_str()) {
(Some(x), Some(y)) => x.cmp(y),
_ => std::cmp::Ordering::Equal,
});
}
for (key, child) in map.iter_mut() {
match key.as_str() {
k if SCHEMA_DEFINITION_MAPS.contains(&k) => {
if let Value::Object(entries) = child {
sort_object_by_key(entries);
for sub in entries.values_mut() {
canonicalize_schema(sub);
}
}
}
"default" | "enum" | "const" | "examples" => {}
_ => canonicalize_schema(child),
}
}
reorder_object(map, SCHEMA_KEYWORD_ORDER);
}
Value::Array(items) => {
for item in items {
canonicalize_schema(item);
}
}
_ => {}
}
}
fn reorder_object(map: &mut serde_json::Map<String, serde_json::Value>, order: &[&str]) {
let mut keys: Vec<String> = map.keys().cloned().collect();
keys.sort_by(|a, b| {
let rank = |k: &str| order.iter().position(|o| *o == k).unwrap_or(order.len());
rank(a).cmp(&rank(b)).then_with(|| a.cmp(b))
});
let mut rebuilt = serde_json::Map::with_capacity(map.len());
for k in keys {
if let Some(v) = map.remove(&k) {
rebuilt.insert(k, v);
}
}
*map = rebuilt;
}
fn sort_object_by_key(map: &mut serde_json::Map<String, serde_json::Value>) {
let mut keys: Vec<String> = map.keys().cloned().collect();
keys.sort();
let mut rebuilt = serde_json::Map::with_capacity(map.len());
for k in keys {
if let Some(v) = map.remove(&k) {
rebuilt.insert(k, v);
}
}
*map = rebuilt;
}
fn collapse_description(s: &str) -> String {
s.split("\n\n")
.map(|para| {
para.split('\n')
.map(str::trim)
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
.join("\n\n")
}
pub fn deserialize_on_worker<F, T>(f: F) -> anyhow::Result<T>
where
F: FnOnce() -> anyhow::Result<T> + Send + 'static,
T: Send + 'static,
{
use anyhow::Context as _;
const WORKER_STACK_SIZE: usize = 8 * 1024 * 1024;
let handle = std::thread::Builder::new()
.stack_size(WORKER_STACK_SIZE)
.name("anodizer-config-deserialize".to_string())
.spawn(f)
.context("failed to spawn config deserialization worker thread")?;
match handle.join() {
Ok(result) => result,
Err(payload) => std::panic::resume_unwind(payload),
}
}
pub fn validate_version(config: &Config) -> Result<(), String> {
match config.version {
None | Some(1) | Some(2) => Ok(()),
Some(v) => Err(format!(
"unsupported config version: {}. Supported versions are 1 and 2.",
v
)),
}
}
pub fn validate_tag_sort(config: &Config) -> Result<(), String> {
if let Some(ref git) = config.git
&& let Some(ref sort) = git.tag_sort
{
match sort.as_str() {
"-version:refname" | "-version:creatordate" | "semver" | "smartsemver" => {}
other => {
return Err(format!(
"unsupported git.tag_sort value: \"{}\". \
Accepted values: \"-version:refname\", \"-version:creatordate\", \
\"semver\", \"smartsemver\".",
other
));
}
}
}
Ok(())
}
pub fn validate_partial(config: &Config) -> Result<(), String> {
if let Some(ref partial) = config.partial
&& let Some(ref by) = partial.by
{
match by.as_str() {
"os" | "target" => {}
other => {
return Err(format!(
"unsupported partial.by value: \"{}\". \
Accepted values: \"os\", \"target\".",
other
));
}
}
}
Ok(())
}
const KNOWN_OS: &[&str] = &[
"aix",
"android",
"darwin",
"dragonfly",
"freebsd",
"illumos",
"ios",
"js",
"linux",
"netbsd",
"openbsd",
"plan9",
"solaris",
"wasip1",
"windows",
];
pub fn validate_release_backends(config: &Config) -> Result<(), String> {
let check = |crate_name: &str, release: &ReleaseConfig| -> Result<(), String> {
let mut set = Vec::new();
if release.github.is_some() {
set.push("github");
}
if release.gitlab.is_some() {
set.push("gitlab");
}
if release.gitea.is_some() {
set.push("gitea");
}
if set.len() > 1 {
return Err(format!(
"crate {}: release config sets multiple mutually-exclusive SCM \
backends ({}). Pick one.",
crate_name,
set.join(" + ")
));
}
Ok(())
};
for krate in &config.crates {
if let Some(ref release) = krate.release {
check(&krate.name, release)?;
}
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
if let Some(ref release) = krate.release {
check(&krate.name, release)?;
}
}
}
}
Ok(())
}
pub fn validate_on_failure_root_only(config: &Config) -> Result<(), String> {
let mut offenders: Vec<&str> = config
.crates
.iter()
.chain(
config
.workspaces
.iter()
.flatten()
.flat_map(|ws| ws.crates.iter()),
)
.filter(|c| c.release.as_ref().is_some_and(|r| r.on_failure.is_some()))
.map(|c| c.name.as_str())
.collect();
offenders.sort_unstable();
offenders.dedup();
if offenders.is_empty() {
return Ok(());
}
Err(format!(
"release.on_failure is a root-level policy and cannot be set per crate \
(set on: {}). Move it to the top-level `release:` block.",
offenders.join(", ")
))
}
pub const ERR_DEFAULTS_AXIS_MISMATCH: &str = "DefaultsAxisMismatch";
pub fn validate_defaults_axis(config: &Config) -> Result<(), String> {
let Some(ref defaults) = config.defaults else {
return Ok(());
};
let has_crate_block = defaults.crates.is_some();
let has_workspace_block = defaults.workspaces.is_some();
if has_crate_block && has_workspace_block {
return Err(format!(
"{ERR_DEFAULTS_AXIS_MISMATCH}: defaults.crates and defaults.workspaces are \
mutually exclusive — pick the axis that matches the top-level config \
(`crates:` or `workspaces:`)",
));
}
let top_uses_workspaces = config.workspaces.as_ref().is_some_and(|w| !w.is_empty());
let top_uses_crates = !config.crates.is_empty();
if has_crate_block && !top_uses_crates {
return Err(format!(
"{ERR_DEFAULTS_AXIS_MISMATCH}: defaults.crates is set but top-level `crates:` \
is {}; move defaults under `defaults.workspaces:` or remove the block",
if top_uses_workspaces {
"absent (top-level uses `workspaces:`)"
} else {
"absent"
},
));
}
if has_workspace_block && !top_uses_workspaces {
return Err(format!(
"{ERR_DEFAULTS_AXIS_MISMATCH}: defaults.workspaces is set but top-level \
`workspaces:` is {}; move defaults under `defaults.crates:` or remove the block",
if top_uses_crates {
"absent (top-level uses `crates:`)"
} else {
"absent"
},
));
}
Ok(())
}
pub fn validate_format_overrides(config: &Config) -> Result<(), String> {
let check = |location: &str, archives: &[ArchiveConfig]| -> Result<(), String> {
for (idx, archive) in archives.iter().enumerate() {
let Some(ref overrides) = archive.format_overrides else {
continue;
};
for over in overrides {
if !KNOWN_OS.contains(&over.os.as_str()) {
let archive_id = archive.id.as_deref().unwrap_or("default");
return Err(format!(
"{}: archives[{}] (id={}): format_overrides.os=\"{}\" is not a recognised OS. \
Accepted values: {}.",
location,
idx,
archive_id,
over.os,
KNOWN_OS.join(", ")
));
}
}
}
Ok(())
};
for krate in &config.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check(&format!("crate {}", krate.name), list)?;
}
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check(&format!("crate {}", krate.name), list)?;
}
}
}
}
if let Some(ref defaults) = config.defaults
&& let Some(ref archive) = defaults.archives
{
check("defaults.archives", std::slice::from_ref(archive))?;
}
Ok(())
}
pub fn validate_homebrew_cask_url_template(config: &Config) -> Result<(), String> {
let check = |location: &str, cask: &HomebrewCaskConfig| -> Result<(), String> {
let has_url_template = cask.url_template.is_some();
let has_url_dot_template = cask.url.as_ref().is_some_and(|u| u.template.is_some());
if has_url_template && has_url_dot_template {
return Err(format!(
"{location}: homebrew_cask sets both `url_template` and `url.template`. \
These are mutually exclusive — use one or the other."
));
}
Ok(())
};
if let Some(ref casks) = config.homebrew_casks {
for (i, cask) in casks.iter().enumerate() {
check(&format!("homebrew_casks[{i}]"), cask)?;
}
}
try_for_each_crate_publish(config, |axis, publish| {
if let Some(cask) = publish.homebrew_cask() {
check(&axis.homebrew_cask_location(), cask)?;
}
Ok(())
})
}
pub const WINGET_UPGRADE_BEHAVIORS: [&str; 3] = ["install", "uninstallPrevious", "deny"];
pub fn validate_winget_upgrade_behavior(config: &Config) -> Result<(), String> {
let check = |location: &str, winget: &WingetConfig| -> Result<(), String> {
if let Some(ref behavior) = winget.upgrade_behavior
&& !WINGET_UPGRADE_BEHAVIORS.contains(&behavior.as_str())
{
return Err(format!(
"{location}: upgrade_behavior `{behavior}` is not a valid winget value. \
Use one of: {}.",
WINGET_UPGRADE_BEHAVIORS.join(", ")
));
}
Ok(())
};
try_for_each_crate_publish(config, |axis, publish| {
if let Some(winget) = publish.winget() {
check(&axis.winget_location(), winget)?;
}
Ok(())
})
}
pub fn validate_winget_dependency_architectures(config: &Config) -> Result<(), String> {
let check = |location: &str, winget: &WingetConfig| -> Result<(), String> {
let Some(ref deps) = winget.dependencies else {
return Ok(());
};
for (i, dep) in deps.iter().enumerate() {
let Some(ref scopes) = dep.architectures else {
continue;
};
for scope in scopes {
if !WINGET_ARCHITECTURES.contains(&scope.as_str()) {
return Err(format!(
"{location}: dependencies[{i}].architectures contains `{scope}`, \
which is not a valid winget architecture. Use one of: {} \
(or leave architectures empty/unset to apply the dependency \
to every installer).",
WINGET_ARCHITECTURES.join(", ")
));
}
}
}
Ok(())
};
try_for_each_crate_publish(config, |axis, publish| {
if let Some(winget) = publish.winget() {
check(&axis.winget_location(), winget)?;
}
Ok(())
})
}
pub fn validate_id_uniqueness(config: &Config) -> Result<(), String> {
fn check_unique(
location: &str,
kind: &str,
ids: impl IntoIterator<Item = (usize, Option<String>)>,
) -> Result<(), String> {
let mut seen: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
for (idx, maybe_id) in ids {
let key = maybe_id.unwrap_or_else(|| "<unset>".to_string());
if let Some(prev_idx) = seen.insert(key.clone(), idx) {
return Err(format!(
"{location}: {kind} id \"{key}\" is used by both entry {prev_idx} and entry {idx} — \
ids must be unique within a {kind} list."
));
}
}
Ok(())
}
let check_archives = |location: &str, archives: &[ArchiveConfig]| -> Result<(), String> {
check_unique(
location,
"archives",
archives.iter().enumerate().map(|(i, a)| (i, a.id.clone())),
)
};
let check_unibins = |location: &str, ubs: &[UniversalBinaryConfig]| -> Result<(), String> {
check_unique(
location,
"universal_binaries",
ubs.iter().enumerate().map(|(i, u)| (i, u.id.clone())),
)
};
for krate in &config.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check_archives(&format!("crates[{}].archives", krate.name), list)?;
}
if let Some(ref ubs) = krate.universal_binaries {
check_unibins(&format!("crates[{}].universal_binaries", krate.name), ubs)?;
}
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
if let ArchivesConfig::Configs(ref list) = krate.archives {
check_archives(
&format!("workspaces[{}].crates[{}].archives", ws.name, krate.name),
list,
)?;
}
if let Some(ref ubs) = krate.universal_binaries {
check_unibins(
&format!(
"workspaces[{}].crates[{}].universal_binaries",
ws.name, krate.name
),
ubs,
)?;
}
}
}
}
Ok(())
}
pub fn validate_builds(config: &Config) -> Result<(), String> {
let check_crate = |location: &str, krate: &CrateConfig| -> Result<(), String> {
let Some(ref builds) = krate.builds else {
return Ok(());
};
let crate_is_prebuilt = builds
.iter()
.any(|b| matches!(b.builder, Some(BuilderKind::Prebuilt)));
if crate_is_prebuilt && krate.cross.is_some() {
return Err(format!(
"{location}: crate-level `cross:` strategy is set but at least one \
build uses `builder: prebuilt`; remove `cross:` (prebuilt imports a \
binary instead of compiling) or change the build's builder to `cargo`."
));
}
for (idx, build) in builds.iter().enumerate() {
match build.builder {
Some(BuilderKind::Prebuilt) => {
let path = build.prebuilt.as_ref().map(|p| p.path.trim()).unwrap_or("");
if path.is_empty() {
return Err(format!(
"{location}.builds[{idx}]: `builder: prebuilt` requires a non-empty \
`prebuilt.path` template. Example: \
`prebuilt: {{ path: \"output/mybin_{{{{ .Target }}}}\" }}`"
));
}
let targets_explicit = build.targets.as_ref().is_some_and(|t| !t.is_empty());
if !targets_explicit {
return Err(format!(
"{location}.builds[{idx}] has `builder: prebuilt` but no explicit \
`targets:` — the prebuilt builder requires per-build target triples \
(no `defaults.targets:` fallback). Add `targets: [<triple>, ...]`."
));
}
if build.cross_tool.as_ref().is_some_and(|s| !s.is_empty()) {
return Err(format!(
"{location}.builds[{idx}]: `cross_tool` is set with \
`builder: prebuilt` — the two are mutually exclusive. \
`cross_tool` controls how cargo cross-compiles; `prebuilt` \
imports an already-built binary. Drop `cross_tool` or use \
`builder: cargo`."
));
}
if build.command.as_ref().is_some_and(|s| !s.is_empty()) {
return Err(format!(
"{location}.builds[{idx}]: `command:` override is set with \
`builder: prebuilt` — the override selects the cargo \
subcommand, which is not invoked under the prebuilt \
builder. Drop `command:` or use `builder: cargo`."
));
}
if build.features.as_ref().is_some_and(|f| !f.is_empty()) {
return Err(format!(
"{location}.builds[{idx}]: `features:` is set with \
`builder: prebuilt` — Cargo features are evaluated at \
compile time, which the prebuilt builder skips. \
Drop `features:` or use `builder: cargo`."
));
}
if build.no_default_features.is_some() {
return Err(format!(
"{location}.builds[{idx}]: `no_default_features:` is set with \
`builder: prebuilt` — Cargo feature flags are evaluated at \
compile time, which the prebuilt builder skips. \
Drop the flag or use `builder: cargo`."
));
}
}
Some(BuilderKind::Cargo) | None => {
if build.prebuilt.is_some() {
tracing::warn!(
"{location}: build[{idx}] has a `prebuilt:` block but `builder:` \
is not `prebuilt`; the block is ignored. Set `builder: prebuilt` \
or remove the block."
);
}
}
}
}
Ok(())
};
for krate in &config.crates {
check_crate(&format!("crates[{}]", krate.name), krate)?;
}
if let Some(ws_list) = config.workspaces.as_ref() {
for ws in ws_list {
for krate in &ws.crates {
check_crate(
&format!("workspaces[{}].crates[{}]", ws.name, krate.name),
krate,
)?;
}
}
}
Ok(())
}
pub fn all_builds_prebuilt(config: &Config) -> bool {
let crate_all_prebuilt = |krate: &CrateConfig| -> Option<bool> {
let builds = krate.builds.as_ref()?;
if builds.is_empty() {
return None;
}
Some(
builds
.iter()
.all(|b| matches!(b.builder, Some(BuilderKind::Prebuilt))),
)
};
let mut saw_any = false;
for krate in config.crate_universe() {
match crate_all_prebuilt(krate) {
Some(true) => saw_any = true,
Some(false) => return false,
None => {}
}
}
saw_any
}
pub fn validate_changelog_groups_depth(config: &Config) -> Result<(), String> {
let check = |location: &str, cfg: &ChangelogConfig| -> Result<(), String> {
let Some(ref groups) = cfg.groups else {
return Ok(());
};
for g in groups {
if let Some(ref subs) = g.groups {
for sub in subs {
if sub.groups.as_ref().is_some_and(|s| !s.is_empty()) {
return Err(format!(
"{location}: changelog group '{}' > '{}' nests further \
subgroups; GoReleaser permits only one level of subgroups \
(see https://goreleaser.com/customization/changelog/). \
Flatten the inner groups into the parent or split into \
sibling top-level groups.",
g.title, sub.title
));
}
}
}
}
Ok(())
};
if let Some(ref cfg) = config.changelog {
check("changelog", cfg)?;
}
if let Some(ref ws_list) = config.workspaces {
for ws in ws_list {
if let Some(ref cfg) = ws.changelog {
check(&format!("workspaces[{}].changelog", ws.name), cfg)?;
}
}
}
Ok(())
}
pub fn validate_changelog_paths(config: &Config) -> Result<(), String> {
let check = |location: &str, cfg: &ChangelogConfig| -> Result<(), String> {
let Some(ref paths) = cfg.paths else {
return Ok(());
};
for (idx, p) in paths.iter().enumerate() {
if p.is_empty() {
return Err(format!(
"{location}: changelog.paths[{idx}] is empty; remove the entry \
or set a real path (empty string matches everything and \
disables filtering)"
));
}
if p.starts_with('/') {
return Err(format!(
"{location}: changelog.paths[{idx}] = {:?} starts with '/'; \
git pathspec is repo-root-relative — write {:?} instead",
p,
p.trim_start_matches('/')
));
}
}
Ok(())
};
if let Some(ref cfg) = config.changelog {
check("changelog", cfg)?;
}
if let Some(ref ws_list) = config.workspaces {
for ws in ws_list {
if let Some(ref cfg) = ws.changelog {
check(&format!("workspaces[{}].changelog", ws.name), cfg)?;
}
}
}
Ok(())
}
pub fn validate_exclude_globs(config: &Config) -> Result<(), String> {
fn check(location: &str, exclude: Option<&[String]>) -> Result<(), String> {
let Some(globs) = exclude else {
return Ok(());
};
for (idx, g) in globs.iter().enumerate() {
if g.is_empty() {
return Err(format!(
"{location}: exclude[{idx}] is empty; remove the entry or set a \
real glob (an empty pattern matches nothing and is a no-op)"
));
}
if let Err(e) = glob::Pattern::new(g) {
return Err(format!(
"{location}: exclude[{idx}] = {g:?} is not a valid glob: {e}"
));
}
}
Ok(())
}
let check_crate = |location: &str, krate: &CrateConfig| -> Result<(), String> {
if let Some(ref release) = krate.release {
check(&format!("{location}.release"), release.exclude.as_deref())?;
}
if let Some(ref blobs) = krate.blobs {
for (i, b) in blobs.iter().enumerate() {
check(&format!("{location}.blobs[{i}]"), b.exclude.as_deref())?;
}
}
Ok(())
};
for krate in &config.crates {
check_crate(&format!("crates[{}]", krate.name), krate)?;
}
if let Some(ref ws_list) = config.workspaces {
for ws in ws_list {
for krate in &ws.crates {
check_crate(
&format!("workspaces[{}].crates[{}]", ws.name, krate.name),
krate,
)?;
}
}
}
if let Some(ref list) = config.artifactories {
for (i, a) in list.iter().enumerate() {
check(&format!("artifactories[{i}]"), a.exclude.as_deref())?;
}
}
if let Some(ref list) = config.cloudsmiths {
for (i, c) in list.iter().enumerate() {
check(&format!("cloudsmiths[{i}]"), c.exclude.as_deref())?;
}
}
if let Some(ref list) = config.gemfury {
for (i, g) in list.iter().enumerate() {
check(&format!("gemfury[{i}]"), g.exclude.as_deref())?;
}
}
if let Some(ref list) = config.uploads {
for (i, u) in list.iter().enumerate() {
check(&format!("uploads[{i}]"), u.exclude.as_deref())?;
}
}
if let Some(ref release) = config.release {
check("release", release.exclude.as_deref())?;
}
Ok(())
}
pub(crate) enum PublishAxis<'a> {
Crate { name: &'a str },
Workspace {
workspace: &'a str,
crate_name: &'a str,
},
Defaults,
}
impl PublishAxis<'_> {
pub(crate) fn location(&self) -> String {
match self {
PublishAxis::Crate { name } => format!("crate '{name}'"),
PublishAxis::Workspace {
workspace,
crate_name,
} => format!("workspaces[{workspace}].crates[{crate_name}]"),
PublishAxis::Defaults => "defaults.publish".to_string(),
}
}
pub(crate) fn homebrew_cask_location(&self) -> String {
match self {
PublishAxis::Crate { name } => {
format!("crates[{name}].publish.homebrew_cask")
}
PublishAxis::Workspace {
workspace,
crate_name,
} => format!("workspaces[{workspace}].crates[{crate_name}].publish.homebrew_cask"),
PublishAxis::Defaults => "defaults.publish.homebrew_cask".to_string(),
}
}
pub(crate) fn winget_location(&self) -> String {
match self {
PublishAxis::Crate { name } => format!("crates[{name}].publish.winget"),
PublishAxis::Workspace {
workspace,
crate_name,
} => format!("workspaces[{workspace}].crates[{crate_name}].publish.winget"),
PublishAxis::Defaults => "defaults.publish.winget".to_string(),
}
}
}
pub(crate) enum PublishRef<'a> {
Crate(&'a PublishConfig),
Defaults(&'a PublishDefaults),
}
impl PublishRef<'_> {
pub(crate) fn homebrew(&self) -> Option<&HomebrewConfig> {
match self {
PublishRef::Crate(p) => p.homebrew.as_ref(),
PublishRef::Defaults(p) => p.homebrew.as_ref(),
}
}
pub(crate) fn chocolatey(&self) -> Option<&ChocolateyConfig> {
match self {
PublishRef::Crate(p) => p.chocolatey.as_ref(),
PublishRef::Defaults(p) => p.chocolatey.as_ref(),
}
}
pub(crate) fn winget(&self) -> Option<&WingetConfig> {
match self {
PublishRef::Crate(p) => p.winget.as_ref(),
PublishRef::Defaults(p) => p.winget.as_ref(),
}
}
pub(crate) fn aur_source(&self) -> Option<&AurSourceConfig> {
match self {
PublishRef::Crate(p) => p.aur_source.as_ref(),
PublishRef::Defaults(p) => p.aur_source.as_ref(),
}
}
pub(crate) fn homebrew_cask(&self) -> Option<&HomebrewCaskConfig> {
match self {
PublishRef::Crate(p) => p.homebrew_cask.as_ref(),
PublishRef::Defaults(p) => p.homebrew_cask.as_ref(),
}
}
}
pub(crate) enum PublishMut<'a> {
Crate(&'a mut PublishConfig),
Defaults(&'a mut PublishDefaults),
}
impl PublishMut<'_> {
pub(crate) fn homebrew_cask_mut(&mut self) -> Option<&mut HomebrewCaskConfig> {
match self {
PublishMut::Crate(p) => p.homebrew_cask.as_mut(),
PublishMut::Defaults(p) => p.homebrew_cask.as_mut(),
}
}
}
pub(crate) fn for_each_crate_publish<F>(config: &Config, mut visit: F)
where
F: FnMut(PublishAxis<'_>, PublishRef<'_>),
{
for krate in &config.crates {
if let Some(ref publish) = krate.publish {
visit(
PublishAxis::Crate { name: &krate.name },
PublishRef::Crate(publish),
);
}
}
if let Some(ref workspaces) = config.workspaces {
for ws in workspaces {
for krate in &ws.crates {
if let Some(ref publish) = krate.publish {
visit(
PublishAxis::Workspace {
workspace: &ws.name,
crate_name: &krate.name,
},
PublishRef::Crate(publish),
);
}
}
}
}
if let Some(ref defaults) = config.defaults
&& let Some(ref publish) = defaults.publish
{
visit(PublishAxis::Defaults, PublishRef::Defaults(publish));
}
}
pub(crate) fn try_for_each_crate_publish<F, E>(config: &Config, mut visit: F) -> Result<(), E>
where
F: FnMut(PublishAxis<'_>, PublishRef<'_>) -> Result<(), E>,
{
for krate in &config.crates {
if let Some(ref publish) = krate.publish {
visit(
PublishAxis::Crate { name: &krate.name },
PublishRef::Crate(publish),
)?;
}
}
if let Some(ref workspaces) = config.workspaces {
for ws in workspaces {
for krate in &ws.crates {
if let Some(ref publish) = krate.publish {
visit(
PublishAxis::Workspace {
workspace: &ws.name,
crate_name: &krate.name,
},
PublishRef::Crate(publish),
)?;
}
}
}
}
if let Some(ref defaults) = config.defaults
&& let Some(ref publish) = defaults.publish
{
visit(PublishAxis::Defaults, PublishRef::Defaults(publish))?;
}
Ok(())
}
pub(crate) fn for_each_crate_publish_mut<F>(config: &mut Config, mut visit: F)
where
F: FnMut(PublishAxis<'_>, PublishMut<'_>),
{
for krate in &mut config.crates {
if let Some(ref mut publish) = krate.publish {
visit(
PublishAxis::Crate { name: &krate.name },
PublishMut::Crate(publish),
);
}
}
if let Some(ref mut workspaces) = config.workspaces {
for ws in workspaces {
for krate in &mut ws.crates {
if let Some(ref mut publish) = krate.publish {
visit(
PublishAxis::Workspace {
workspace: &ws.name,
crate_name: &krate.name,
},
PublishMut::Crate(publish),
);
}
}
}
}
if let Some(ref mut defaults) = config.defaults
&& let Some(ref mut publish) = defaults.publish
{
visit(PublishAxis::Defaults, PublishMut::Defaults(publish));
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SubmitterAdvisory {
pub publisher: String,
pub message: String,
}
pub fn submitter_required_warnings(config: &Config) -> Vec<SubmitterAdvisory> {
fn advisory(location: &str, name: &str, publisher: &str) -> SubmitterAdvisory {
SubmitterAdvisory {
publisher: publisher.to_string(),
message: format!(
"{location}: publisher '{name}' submits to an external moderation queue; \
`required: true` fails the release when the submission itself fails, \
but the eventual moderation outcome happens outside the release run \
and cannot be gated."
),
}
}
let mut warnings = Vec::new();
for_each_crate_publish(config, |axis, publish| {
let loc = axis.location();
if publish.chocolatey().and_then(|c| c.required) == Some(true) {
warnings.push(advisory(&loc, "chocolatey", "chocolatey"));
}
if publish.winget().and_then(|w| w.required) == Some(true) {
warnings.push(advisory(&loc, "winget", "winget"));
}
if publish.aur_source().and_then(|a| a.required) == Some(true) {
warnings.push(advisory(&loc, "aur_source", "upstream-aur"));
}
});
if let Some(ref sources) = config.aur_sources {
for (idx, src) in sources.iter().enumerate() {
if src.required == Some(true) {
let loc = format!("top-level aur_sources[{idx}]");
warnings.push(advisory(&loc, "aur_source", "upstream-aur"));
}
}
}
warnings
}
pub fn apply_archive_legacy_aliases(_config: &mut Config) {
}
pub fn validate_no_docker_v1(raw_yaml: &serde_yaml_ng::Value) -> Result<(), String> {
if raw_yaml.get("dockers").is_some() {
return Err(
"config: legacy GoReleaser `dockers:` block is not supported — anodizer ships \
dockers_v2: only (multi-arch buildx flow). Port the config to `dockers_v2:` per \
https://anodize.dev/docs/migration/docker.html."
.to_string(),
);
}
Ok(())
}
pub fn warn_on_legacy_homebrew_formula(config: &Config) {
for msg in legacy_homebrew_formula_warnings(config) {
tracing::warn!("{}", msg);
}
}
pub(crate) fn legacy_homebrew_formula_warnings(config: &Config) -> Vec<String> {
fn formula_warning(location: &str) -> String {
format!(
"DEPRECATION: {location}: publish.homebrew (Homebrew Formula) is deprecated upstream \
in GoReleaser v2.16; migrate to homebrew_casks. Cask is now the canonical Homebrew \
distribution channel for pre-compiled binaries. See \
https://anodize.dev/docs/publish/homebrew-casks/ for migration."
)
}
let mut warnings = Vec::new();
for_each_crate_publish(config, |axis, publish| {
if publish.homebrew().is_some() {
warnings.push(formula_warning(&axis.location()));
}
});
warnings
}
pub fn warn_on_legacy_snapshot_name_template(raw_yaml: &serde_yaml_ng::Value) {
if let Some(snap) = raw_yaml.get("snapshot")
&& snap.get("name_template").is_some()
{
tracing::warn!(
"DEPRECATION: snapshot.name_template is deprecated; use \
snapshot.version_template instead. Both spellings are accepted \
but the legacy key will be removed in a future release."
);
}
}
pub fn warn_on_legacy_furies_alias(raw_yaml: &serde_yaml_ng::Value) {
if raw_yaml.get("furies").is_some() {
tracing::warn!(
"DEPRECATION: the top-level `furies:` config key is deprecated since GoReleaser \
Pro v2.14; rename it to `gemfury:`. Both spellings are accepted but the legacy \
key will be removed in a future release."
);
}
}
pub fn warn_on_legacy_nfpm_builds(raw_yaml: &serde_yaml_ng::Value) {
fn warn_for_nfpm_value(value: &serde_yaml_ng::Value) {
match value {
serde_yaml_ng::Value::Mapping(_) => {
if value.get("builds").is_some() {
tracing::warn!(
"DEPRECATION: nfpm `builds:` is deprecated; use `ids:` instead. \
Both spellings are accepted but the legacy key will be removed in \
a future release."
);
}
}
serde_yaml_ng::Value::Sequence(items) => {
for item in items {
warn_for_nfpm_value(item);
}
}
_ => {}
}
}
fn descend(value: &serde_yaml_ng::Value) {
match value {
serde_yaml_ng::Value::Mapping(map) => {
for (key, child) in map {
if matches!(key.as_str(), Some("nfpm") | Some("nfpms")) {
warn_for_nfpm_value(child);
}
descend(child);
}
}
serde_yaml_ng::Value::Sequence(items) => {
for item in items {
descend(item);
}
}
_ => {}
}
}
descend(raw_yaml);
}
pub fn warn_on_legacy_disable_alias(raw_yaml: &serde_yaml_ng::Value) {
for msg in legacy_disable_alias_warnings(raw_yaml) {
tracing::warn!("{}", msg);
}
}
pub(crate) fn legacy_disable_alias_warnings(raw_yaml: &serde_yaml_ng::Value) -> Vec<String> {
const ALLOWLIST: &[&str] = &[
"mcp",
"makeselfs",
"makeselves",
"appimages",
"msis",
"pkgs",
"nsis",
"dockerhub",
"release",
"dockers_v2",
"docker_v2",
"changelog",
"snapcrafts",
"npms",
"gemfury",
"furies",
"publishers",
"sboms",
"aur",
"aur_source",
"aur_sources",
"blobs",
"docker_digest",
"checksum",
"flatpaks",
];
fn disable_warning(path: &str) -> String {
format!(
"DEPRECATION: {path}: legacy `disable:` is deprecated; rename it to `skip:`. \
Both spellings are accepted but the legacy key will be removed in a future release."
)
}
fn descend(
value: &serde_yaml_ng::Value,
path: &str,
enclosing_block: Option<&str>,
warnings: &mut Vec<String>,
) {
match value {
serde_yaml_ng::Value::Mapping(map) => {
for (key, child) in map {
let Some(key) = key.as_str() else { continue };
let child_path = if path.is_empty() {
key.to_string()
} else {
format!("{path}.{key}")
};
if key == "disable"
&& enclosing_block.is_some_and(|block| ALLOWLIST.contains(&block))
{
warnings.push(disable_warning(&child_path));
}
descend(child, &child_path, Some(key), warnings);
}
}
serde_yaml_ng::Value::Sequence(items) => {
for (idx, item) in items.iter().enumerate() {
let item_path = format!("{path}[{idx}]");
descend(item, &item_path, enclosing_block, warnings);
}
}
_ => {}
}
}
let mut warnings = Vec::new();
descend(raw_yaml, "", None, &mut warnings);
warnings
}
pub fn validate_no_mcp_github(raw_yaml: &serde_yaml_ng::Value) -> Result<(), String> {
if raw_yaml.get("mcp").and_then(|m| m.get("github")).is_some() {
return Err(
"config: nested `mcp.github:` block is not supported — anodizer mirrors GoReleaser \
v2.13.1+ where registry metadata moved to top-level `mcp:` fields (`mcp.name`, \
`mcp.repository.url`, `mcp.repository.source`). Port the nested keys to the \
canonical surface."
.to_string(),
);
}
Ok(())
}
pub fn warn_on_legacy_docker_retry(config: &Config) {
for msg in legacy_docker_retry_warnings(config) {
tracing::warn!("{}", msg);
}
}
pub(crate) fn legacy_docker_retry_warnings(config: &Config) -> Vec<String> {
fn pipe_warning(location: &str, kind: &str) -> String {
format!(
"DEPRECATION: {location}: nested `{kind}.retry:` is deprecated since GoReleaser \
v2.15.3; move retry settings to the top-level `retry:` block. The per-pipe \
value still wins at resolve time for back-compat, but the legacy spelling will \
be removed in a future release."
)
}
let mut warnings = Vec::new();
let scan_crate = |krate: &CrateConfig, prefix: &str, warnings: &mut Vec<String>| {
if let Some(ref v2) = krate.dockers_v2 {
for (i, cfg) in v2.iter().enumerate() {
if cfg.retry.is_some() {
warnings.push(pipe_warning(
&format!("{prefix}.dockers_v2[{i}]"),
"dockers_v2",
));
}
}
}
if let Some(ref manifests) = krate.docker_manifests {
for (i, cfg) in manifests.iter().enumerate() {
if cfg.retry.is_some() {
warnings.push(pipe_warning(
&format!("{prefix}.docker_manifests[{i}]"),
"docker_manifests",
));
}
}
}
};
for krate in &config.crates {
scan_crate(krate, &format!("crates[{}]", krate.name), &mut warnings);
}
if let Some(ref workspaces) = config.workspaces {
for ws in workspaces {
for krate in &ws.crates {
scan_crate(
krate,
&format!("workspaces[{}].crates[{}]", ws.name, krate.name),
&mut warnings,
);
}
}
}
if let Some(ref defaults) = config.defaults
&& let Some(ref v2) = defaults.dockers_v2
&& v2.retry.is_some()
{
warnings.push(pipe_warning("defaults.dockers_v2", "dockers_v2"));
}
warnings
}
pub fn apply_homebrew_cask_legacy_singulars(config: &mut Config) {
fn fold_one(location: &str, cask: &mut HomebrewCaskConfig) -> Vec<String> {
let mut warnings = Vec::new();
if let Some(legacy) = cask.legacy_binary.take() {
let entry = HomebrewCaskBinary::Name(legacy.clone());
match cask.binaries {
Some(ref mut list) => list.insert(0, entry),
None => cask.binaries = Some(vec![entry]),
}
warnings.push(format!(
"DEPRECATION: {location}: singular `binary: {legacy}` is deprecated since \
GoReleaser v2.12.6; use the plural `binaries: [{legacy}]` form. The legacy \
value has been folded into binaries[0]."
));
}
if let Some(legacy) = cask.legacy_manpage.take() {
match cask.manpages {
Some(ref mut list) => list.push(legacy.clone()),
None => cask.manpages = Some(vec![legacy.clone()]),
}
warnings.push(format!(
"DEPRECATION: {location}: singular `manpage: {legacy}` is deprecated; \
use the plural `manpages: [{legacy}]` form. The legacy value has been \
folded into manpages."
));
}
warnings
}
let mut warnings = Vec::new();
if let Some(ref mut casks) = config.homebrew_casks {
for (i, cask) in casks.iter_mut().enumerate() {
warnings.extend(fold_one(&format!("homebrew_casks[{i}]"), cask));
}
}
for_each_crate_publish_mut(config, |axis, mut publish| {
if let Some(cask) = publish.homebrew_cask_mut() {
warnings.extend(fold_one(&axis.homebrew_cask_location(), cask));
}
});
for msg in warnings {
tracing::warn!("{}", msg);
}
}
mod env_files;
pub use env_files::*;
mod defaults;
pub use defaults::*;
mod build;
pub use build::*;
mod archives;
pub use archives::*;
mod completions;
pub use completions::*;
mod release;
pub use release::*;
mod publishers;
pub use publishers::*;
mod docker;
pub use docker::*;
mod nfpm;
pub use nfpm::*;
mod snapcraft;
pub use snapcraft::*;
mod installers;
pub use installers::*;
mod blob;
pub use blob::*;
mod partial;
pub use partial::*;
mod binstall;
pub use binstall::*;
mod notarize;
pub use notarize::*;
mod source;
pub use source::*;
mod sbom;
pub use sbom::*;
mod attestation;
pub use attestation::*;
mod version_sync;
pub use version_sync::*;
mod changelog;
pub use changelog::*;
pub use crate::signing::{AuthenticodeConfig, DockerSignConfig, SignConfig};
mod upx;
pub use upx::*;
mod snapshot_nightly;
pub use snapshot_nightly::*;
mod cargo_metadata;
pub use cargo_metadata::derive_metadata_from_cargo_toml;
pub fn maintainer_name_only(maintainer: &str) -> Option<String> {
let name = maintainer.split('<').next().unwrap_or(maintainer).trim();
(!name.is_empty()).then(|| name.to_string())
}
mod templatefiles;
pub use templatefiles::*;
mod announce;
pub use announce::*;
mod dockerhub;
pub use dockerhub::*;
mod artifactory;
pub use artifactory::*;
mod cloudsmith;
pub use cloudsmith::*;
mod publisher;
pub use publisher::*;
mod hooks;
pub use hooks::*;
mod git_config;
pub use git_config::*;
mod monorepo;
pub use monorepo::*;
mod tag;
pub use tag::*;
mod workspace;
pub use workspace::*;
mod retry;
pub use retry::*;
mod post_publish_poll;
pub use post_publish_poll::*;
mod verify_release;
pub use verify_release::*;
mod string_or_bool;
pub use string_or_bool::*;
pub use crate::packagers::{
AppImageConfig, AppImageExtra, MakeselfConfig, MakeselfFile, RuntimeHarvest, SrpmConfig,
};
pub(crate) use crate::packagers::{
appimages_schema, deserialize_appimages, deserialize_makeselfs, makeselfs_schema,
};
mod milestone;
pub use milestone::*;
mod upload;
pub use upload::*;
mod aur_source;
pub use aur_source::*;
mod mcp;
pub use mcp::*;
mod npm;
pub use npm::*;
mod gemfury;
pub use gemfury::*;
mod discovery;
pub use discovery::*;
#[cfg(test)]
mod tests;