use super::*;
pub(crate) fn run_rollback_only(ctx: &mut Context) -> Result<()> {
let outcome = (|| -> Result<()> {
let run_id = ctx
.options
.from_run
.clone()
.ok_or_else(|| anyhow::anyhow!("--rollback-only requires --from-run=<id>"))?;
let updated_report = anodizer_stage_publish::rollback_only::run(ctx, &run_id)?;
ctx.set_publish_report(updated_report);
Ok(())
})();
anodizer_stage_announce::emit_summary(ctx);
outcome
}
pub(crate) fn run_release_env_preflight(
ctx: &Context,
opts: &ReleaseOpts,
log: &StageLogger,
) -> Result<()> {
if !opts.no_preflight && !opts.dry_run && !opts.snapshot && !opts.split {
let scope = if opts.announce_only {
crate::commands::preflight::PreflightScope::AnnounceOnly
} else if opts.publish_only {
crate::commands::preflight::PreflightScope::PublishOnly
} else {
crate::commands::preflight::PreflightScope::Full
};
let report = crate::commands::preflight::run_env_preflight(ctx, scope, log);
if !report.ok() {
anyhow::bail!(
"preflight: {} environment failure(s) across {} check(s); \
fix the issues above or re-run with --no-preflight to override",
report.failures.len(),
report.checks
);
}
} else if opts.no_preflight {
log.warn(
"preflight skipped via --no-preflight; missing tools / secrets / key material \
will surface mid-pipeline (no idempotent recovery)",
);
}
Ok(())
}
pub(crate) fn run_before_hooks(
ctx: &Context,
config: &Config,
opts: &ReleaseOpts,
log: &StageLogger,
) -> Result<()> {
if !opts.publish_only
&& !opts.announce_only
&& !ctx.should_skip("before")
&& let Some(before) = &config.before
&& let Some(ref hooks) = before.hooks
{
pipeline::run_hooks(
hooks,
"before",
HookRunContext::new(opts.dry_run, log, Some(ctx.template_vars())),
)?;
}
Ok(())
}
pub(crate) fn render_release_notes_tmpl(
ctx: &mut Context,
config: &Config,
opts: &ReleaseOpts,
release_notes_path: Option<(PathBuf, String)>,
log: &StageLogger,
) -> Result<()> {
if !opts.publish_only
&& !opts.announce_only
&& let Some((tmpl_path, raw_content)) = release_notes_path
{
let rendered = template::render(&raw_content, ctx.template_vars()).with_context(|| {
format!(
"failed to render release notes template: {}",
tmpl_path.display()
)
})?;
let dist = &config.dist;
std::fs::create_dir_all(dist).ok();
let rendered_path = dist.join("release-notes.md");
std::fs::write(&rendered_path, &rendered).with_context(|| {
format!(
"failed to write rendered release notes: {}",
rendered_path.display()
)
})?;
ctx.options.release_notes_path = Some(rendered_path);
log.verbose("rendered release notes template");
}
Ok(())
}
pub(crate) fn enforce_dirty_repo_gate(ctx: &Context) -> Result<()> {
if git::is_git_dirty() && !ctx.is_snapshot() && !ctx.is_nightly() && !ctx.is_dry_run() {
let status = git::git_status_porcelain();
anyhow::bail!(
"git repository is dirty; use --snapshot to release from a dirty tree, or commit your changes first.\n\nDirty files:\n{}",
status
);
}
Ok(())
}
pub(crate) fn apply_nightly_template_vars(
ctx: &mut Context,
config: &Config,
log: &StageLogger,
) -> Result<()> {
let nightly_cfg = config.nightly.as_ref();
ctx.template_vars_mut().set_bool("IsNightly", true);
let version_tmpl = nightly_cfg
.and_then(|c| c.version_template.as_deref())
.filter(|s| !s.trim().is_empty())
.unwrap_or("{{ incpatch(v=Version) }}-{{ ShortCommit }}-nightly");
let nightly_version = template::render(version_tmpl, ctx.template_vars())
.with_context(|| format!("failed to render nightly version_template: {version_tmpl}"))?;
let nightly_version = nightly_version.trim().to_string();
if nightly_version.is_empty() {
anyhow::bail!(
"nightly version_template rendered to an empty string (template: {version_tmpl})"
);
}
ctx.template_vars_mut().set("Version", &nightly_version);
ctx.template_vars_mut().set("RawVersion", &nightly_version);
let tag_tmpl = nightly_cfg
.and_then(|c| c.tag_name.as_deref())
.unwrap_or("nightly");
let nightly_tag = template::render(tag_tmpl, ctx.template_vars())
.with_context(|| format!("failed to render nightly tag_name: {tag_tmpl}"))?;
let nightly_tag = nightly_tag.trim().to_string();
if nightly_tag.is_empty() {
anyhow::bail!(
"nightly tag_name rendered to an empty string (template: {tag_tmpl}). \
An empty tag would be rejected by GitHub's Releases API."
);
}
ctx.template_vars_mut().set("Tag", &nightly_tag);
let name_tmpl = nightly_cfg
.and_then(|c| c.name_template.as_deref())
.unwrap_or("{{ ProjectName }}-nightly");
let release_name = template::render(name_tmpl, ctx.template_vars())
.with_context(|| format!("failed to render nightly name_template: {name_tmpl}"))?;
ctx.template_vars_mut().set("ReleaseName", &release_name);
log.verbose(&format!(
"nightly version={}, tag={}, name={}",
nightly_version, nightly_tag, release_name
));
Ok(())
}
pub(crate) fn apply_snapshot_template_vars(
ctx: &mut Context,
config: &Config,
log: &StageLogger,
) -> Result<()> {
let snapshot_tmpl = config
.snapshot
.as_ref()
.map(|s| s.version_template.as_str())
.filter(|s| !s.trim().is_empty())
.unwrap_or("{{ Version }}-SNAPSHOT-{{ ShortCommit }}");
let rendered_name =
template::render(snapshot_tmpl, ctx.template_vars()).with_context(|| {
format!(
"failed to render snapshot version_template: {}",
snapshot_tmpl
)
})?;
if rendered_name.trim().is_empty() {
anyhow::bail!("empty snapshot name after rendering version_template");
}
ctx.template_vars_mut().set("Version", &rendered_name);
ctx.template_vars_mut().set("ReleaseName", &rendered_name);
log.verbose(&format!(
"snapshot version={}, release_name={}",
rendered_name, rendered_name
));
Ok(())
}
pub(crate) fn run_publisher_preflight(
ctx: &mut Context,
opts: &ReleaseOpts,
log: &StageLogger,
) -> Result<bool> {
if opts.announce_only {
log.status("skipped preflight — --announce-only does not publish");
return Ok(false);
}
let should_run_preflight = should_run_preflight_auto(
opts.no_preflight,
opts.snapshot,
opts.dry_run,
opts.split,
ctx.should_skip("publish"),
);
if !(opts.preflight || should_run_preflight) {
return Ok(false);
}
let report = anodizer_stage_publish::preflight::run_preflight(ctx, log)?;
if report.entries.is_empty() {
log.verbose("skipped one-way-door preflight — no one-way-door publishers configured");
} else {
for line in report.to_string().trim_end_matches('\n').lines() {
log.status(line);
}
}
let strict_preflight = ctx.preflight_is_strict();
if report.has_blockers(strict_preflight) {
let blockers = report.blockers(strict_preflight);
let labels: Vec<String> = blockers
.iter()
.map(|b| format!("{} ({})", b.publisher, b.state.label()))
.collect();
anyhow::bail!(
"preflight: {} publisher(s) blocked the release: {}. \
Resolve upstream (await moderation / merge or close the PR / bump version) \
or re-run with --no-preflight to override.",
blockers.len(),
labels.join(", ")
);
}
if !report.blockers.is_empty() {
anyhow::bail!(
"preflight: {} resilience blocker(s): {}",
report.blockers.len(),
report.blockers.join("; "),
);
}
log.status(&format!(
"preflight found {} publisher(s) clean",
report.clean_count()
));
if opts.preflight { Ok(true) } else { Ok(false) }
}
pub(crate) fn gate_required_failures(ctx: &Context) -> Result<()> {
anodizer_core::publish_report::gate_required_failures(
ctx,
"The release pipeline ran to completion, so rollback / \
announce-gating / summary all observed final state; this non-zero \
exit ensures CI and shell callers see the failure.",
)
}
pub(crate) fn select_custom_publishers(
ctx: &Context,
publishers: &[anodizer_core::config::PublisherConfig],
log: &StageLogger,
) -> Vec<anodizer_core::config::PublisherConfig> {
publishers
.iter()
.enumerate()
.filter(|(i, p)| {
let name = p.name.clone().unwrap_or_else(|| format!("publisher[{i}]"));
if ctx.publisher_deselected(&name) {
let reason = ctx.deselected_reason(&name);
ctx.skip_memento.remember("publisher", &name, &reason);
log.status(&reason);
false
} else {
true
}
})
.map(|(_, p)| p.clone())
.collect()
}
pub(crate) fn run_post_pipeline(
ctx: &mut Context,
config: &Config,
dry_run: bool,
log: &anodizer_core::log::StageLogger,
) -> Result<()> {
let _section = log.group("finalize");
let log = &log.with_stage("finalize");
helpers::run_report_sizes(ctx, config, log);
helpers::write_metadata_and_artifacts(ctx, config, log)?;
if let Some(ref publishers) = config.publishers
&& !publishers.is_empty()
{
let selected = select_custom_publishers(ctx, publishers, log);
if !selected.is_empty() {
log.status("running custom publishers...");
crate::commands::publisher::run_publishers(
&selected,
ctx.artifacts.all(),
ctx.template_vars(),
dry_run,
log,
ctx.options.parallelism,
Some(&ctx.skip_memento),
)?;
}
}
if let Some(ref milestones) = config.milestones {
if ctx.is_nightly() {
log.status("milestone close skipped — nightly run (GoReleaser parity)");
} else {
milestones::close_milestones(milestones, ctx, dry_run, log)?;
}
}
run_post_pipeline_after_hooks_only(ctx, config, dry_run, log)
}
pub(super) fn run_post_pipeline_after_hooks_only(
ctx: &Context,
config: &Config,
dry_run: bool,
log: &anodizer_core::log::StageLogger,
) -> Result<()> {
if let Some(after) = &config.after
&& let Some(ref hooks) = after.hooks
{
pipeline::run_hooks(
hooks,
"after",
HookRunContext::new(dry_run, log, Some(ctx.template_vars())),
)?;
}
Ok(())
}