use super::*;
pub(crate) fn should_run_preflight_auto(
no_preflight: bool,
snapshot: bool,
dry_run: bool,
split: bool,
publish_skipped: bool,
) -> bool {
!no_preflight && !snapshot && !dry_run && !split && !publish_skipped
}
pub(crate) fn apply_prepare_mode_to_skip(skip: &mut Vec<String>) {
for &stage in anodizer_core::stages::UPSTREAM_STAGES {
if !skip.iter().any(|s| s == stage) {
skip.push(stage.to_string());
}
}
}
pub(crate) fn install_verify_gate(ctx: &mut Context) {
ctx.verify_gate = Some(std::sync::Arc::new(|ctx: &mut Context| {
anodizer_stage_verify_release::run_asset_gate(ctx)
}));
}
pub(crate) fn validate_strict_vs_allowlist(opts: &ReleaseOpts) -> Result<()> {
if opts.strict && !opts.allow_nondeterministic.is_empty() {
anyhow::bail!(
"--strict and --allow-nondeterministic are mutually exclusive (drop --strict if a runtime exemption is required)"
);
}
Ok(())
}
pub(crate) fn apply_workspace_overlay_for_opts(
config: &mut Config,
opts: &ReleaseOpts,
log: &StageLogger,
) -> Result<Vec<String>> {
helpers::apply_workspace_scope(config, opts.workspace.as_deref(), &opts.crate_names, log)
}
pub(crate) fn apply_release_meta_overrides(config: &mut Config, opts: &ReleaseOpts) -> Result<()> {
if opts.draft {
let release = config.release.get_or_insert_with(Default::default);
release.draft = Some(true);
}
if let Some(ref header_path) = opts.release_header {
let header_content = std::fs::read_to_string(header_path).with_context(|| {
format!(
"failed to read release header file: {}",
header_path.display()
)
})?;
let release = config.release.get_or_insert_with(Default::default);
release.header = Some(anodizer_core::config::ContentSource::Inline(header_content));
}
if let Some(ref header_tmpl_path) = opts.release_header_tmpl {
let raw = std::fs::read_to_string(header_tmpl_path).with_context(|| {
format!(
"failed to read release header template file: {}",
header_tmpl_path.display()
)
})?;
let release = config.release.get_or_insert_with(Default::default);
release.header = Some(anodizer_core::config::ContentSource::Inline(raw));
}
if let Some(ref footer_path) = opts.release_footer {
let footer_content = std::fs::read_to_string(footer_path).with_context(|| {
format!(
"failed to read release footer file: {}",
footer_path.display()
)
})?;
let release = config.release.get_or_insert_with(Default::default);
release.footer = Some(anodizer_core::config::ContentSource::Inline(footer_content));
}
if let Some(ref footer_tmpl_path) = opts.release_footer_tmpl {
let raw = std::fs::read_to_string(footer_tmpl_path).with_context(|| {
format!(
"failed to read release footer template file: {}",
footer_tmpl_path.display()
)
})?;
let release = config.release.get_or_insert_with(Default::default);
release.footer = Some(anodizer_core::config::ContentSource::Inline(raw));
}
Ok(())
}
pub(crate) fn enforce_dist_state(
config: &Config,
opts: &ReleaseOpts,
log: &StageLogger,
) -> Result<()> {
if opts.clean && !opts.dry_run {
let dist = &config.dist;
if dist.exists() {
std::fs::remove_dir_all(dist)?;
}
} else if opts.clean && opts.dry_run {
log.status("(dry-run) would clean dist directory");
}
if !opts.clean
&& !opts.merge
&& !opts.publish_only
&& !opts.rollback_only
&& !opts.announce_only
&& !opts.preflight_secrets
{
let dist = &config.dist;
if dist.exists()
&& let Ok(mut entries) = dist.read_dir()
&& entries.next().is_some()
{
return Err(anodizer_core::error_class::deterministic_msg(format!(
"dist directory '{}' is not empty; use --clean to remove it first",
dist.display()
)));
}
}
Ok(())
}
pub(crate) fn read_release_notes_template(opts: &ReleaseOpts) -> Result<Option<(PathBuf, String)>> {
if let Some(ref tmpl_path) = opts.release_notes_tmpl {
let content = std::fs::read_to_string(tmpl_path).with_context(|| {
format!(
"failed to read release notes template: {}",
tmpl_path.display()
)
})?;
Ok(Some((tmpl_path.clone(), content)))
} else {
Ok(None)
}
}
pub(crate) fn parse_rollback_mode(rollback: Option<&str>) -> Result<Option<RollbackMode>> {
match rollback {
Some("none") => Ok(Some(RollbackMode::None)),
Some("best-effort") => Ok(Some(RollbackMode::BestEffort)),
Some(other) => anyhow::bail!(
"invalid --rollback value: {} (expected: none, best-effort)",
other
),
None => Ok(None),
}
}
pub(crate) fn resolve_simulate_failure(simulate: &mut Vec<String>) -> Result<Vec<String>> {
if std::env::var("ANODIZE_TEST_HARNESS").as_deref() == Ok("1") {
Ok(std::mem::take(simulate))
} else if !simulate.is_empty() {
anyhow::bail!(
"--simulate-failure requires ANODIZE_TEST_HARNESS=1 (test-harness gated flag)"
);
} else {
Ok(Vec::new())
}
}
pub(crate) fn parse_allow_nondeterministic(entries: &[String]) -> Result<Vec<(String, String)>> {
entries
.iter()
.map(|s| {
let (name, reason) = s.split_once('=').ok_or_else(|| {
anyhow::anyhow!("--allow-nondeterministic must be NAME=REASON, got: {}", s)
})?;
if reason.trim().is_empty() {
anyhow::bail!("--allow-nondeterministic reason cannot be empty for: {}", s);
}
Ok::<_, anyhow::Error>((name.to_string(), reason.to_string()))
})
.collect()
}
pub(crate) fn resolve_project_root(
config_path: &std::path::Path,
log: Option<&StageLogger>,
) -> Option<PathBuf> {
let from_parent = config_path
.parent()
.filter(|p| !p.as_os_str().is_empty())
.map(std::path::Path::to_path_buf);
let candidate = match from_parent {
Some(p) => p,
None => {
let cwd = std::env::current_dir().ok()?;
if let Some(log) = log {
log.warn(&format!(
"project_root falling back to CWD `{}` because --config=`{}` is a bare filename",
cwd.display(),
config_path.display()
));
log.warn(
"repo-relative file lookups (snapcraft icons, extra-file globs, ...) \
will resolve against the process CWD — pass --config with a parent \
directory (e.g. `--config=./anodize.yaml`) if this is incorrect",
);
}
cwd
}
};
Some(std::fs::canonicalize(&candidate).unwrap_or(candidate))
}
pub(crate) fn resolve_host_targets(
opts: &mut ReleaseOpts,
config: &Config,
selected_crates: &[String],
log: &StageLogger,
) -> Result<()> {
let host = anodizer_core::partial::resolve_host_target()
.context("--host-targets: failed to detect the host target triple")?;
apply_host_targets_filter(opts, config, selected_crates, &host, log)
}
pub(crate) fn apply_host_targets_filter(
opts: &mut ReleaseOpts,
config: &Config,
selected_crates: &[String],
host: &str,
log: &StageLogger,
) -> Result<()> {
let configured = helpers::collect_build_targets(config, selected_crates);
if configured.is_empty() {
return Ok(());
}
let (kept, skipped) = anodizer_core::partial::host_buildable_targets(host, &configured);
if let Some(msg) = anodizer_core::partial::host_targets_skip_message(host, &skipped) {
log.warn(&msg);
}
if kept.is_empty() {
let reasons = anodizer_core::partial::host_targets_skip_reasons(host, &skipped);
anyhow::bail!(
"--host-targets: none of the {} configured target(s) can be built on this host \
({}); all require a different native host: {}. Adjust build.targets, or run on \
a host that satisfies the constraint above.",
configured.len(),
host,
reasons,
);
}
opts.targets = Some(kept);
Ok(())
}
pub(crate) fn build_context_options(
opts: &ReleaseOpts,
skip_stages: Vec<String>,
selected_sorted: Vec<String>,
rollback_mode: Option<RollbackMode>,
simulate_failure_publishers: Vec<String>,
runtime_nondeterministic_allowlist: Vec<(String, String)>,
project_root: Option<PathBuf>,
) -> ContextOptions {
ContextOptions {
snapshot: opts.snapshot,
nightly: opts.nightly,
dry_run: opts.dry_run,
quiet: opts.quiet,
verbose: opts.verbose,
debug: opts.debug,
skip_stages,
selected_crates: selected_sorted,
token: opts.token.clone(),
parallelism: opts.parallelism,
single_target: opts.single_target.clone(),
release_notes_path: opts.release_notes.clone(),
fail_fast: opts.fail_fast,
partial_target: opts
.targets
.clone()
.map(anodizer_core::partial::PartialTarget::Targets),
merge: opts.merge,
publish_only: opts.publish_only,
preflight_secrets: opts.preflight_secrets,
project_root,
strict: opts.strict,
strict_preflight: opts.strict_preflight,
resume_release: opts.resume_release || opts.publish_only,
replace_existing_artifacts: opts.replace_existing,
skip_post_publish_poll: opts.no_post_publish_poll,
gate_submitter: if opts.no_gate_submitter {
Some(false)
} else {
None
},
rollback_mode,
simulate_failure_publishers,
rollback_only: opts.rollback_only,
allow_rerun: opts.allow_rerun,
show_skipped: opts.show_skipped,
from_run: opts.from_run.clone(),
runtime_nondeterministic_allowlist,
summary_json_path: opts.summary_json.clone(),
allow_ai_failure: opts.allow_ai_failure,
changelog_from: None,
changelog_full_history: false,
changelog_to: None,
changelog_preview: false,
notify: false,
allow_snapshot_publish: opts.allow_snapshot_publish,
publisher_allowlist: opts.publishers.clone(),
}
}