use super::*;
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 fn validate_on_failure_not_rollback(config: &Config) -> Result<(), String> {
let is_rollback = |r: &ReleaseConfig| r.on_failure == Some(OnFailureConfig::Rollback);
let root_is_rollback = config.release.as_ref().is_some_and(is_rollback);
let crate_is_rollback = config
.crates
.iter()
.chain(
config
.workspaces
.iter()
.flatten()
.flat_map(|ws| ws.crates.iter()),
)
.any(|c| c.release.as_ref().is_some_and(is_rollback));
if !root_is_rollback && !crate_is_rollback {
return Ok(());
}
Err(
"release.on_failure: rollback is no longer supported — automatic rollback was \
removed — re-running `anodizer release` converges; use `anodizer tag rollback` \
for deliberate withdrawal."
.to_string(),
)
}
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(())
}