use super::*;
pub(crate) fn selection_depends_on_head_tags(opts: &ReleaseOpts) -> bool {
opts.crate_names.is_empty() && !opts.all && !is_tagless_mode(opts)
}
fn is_tagless_mode(opts: &ReleaseOpts) -> bool {
opts.snapshot
|| opts.nightly
|| opts.dry_run
|| opts.publish_only
|| opts.announce_only
|| opts.split
|| opts.merge
}
fn mode_tolerates_an_unreadable_repository(opts: &ReleaseOpts) -> bool {
is_tagless_mode(opts)
}
pub(crate) fn recover_crate_selection(
selection: Result<Vec<String>>,
opts: &ReleaseOpts,
log: &StageLogger,
) -> Result<Vec<String>> {
match selection {
Ok(selected) => Ok(selected),
Err(e)
if mode_tolerates_an_unreadable_repository(opts)
&& e.chain().any(|c| c.is::<git::RepositoryUnreadable>()) =>
{
log.warn(&format!("continuing without the tags at HEAD: {e:#}"));
Ok(Vec::new())
}
Err(e) => Err(e),
}
}
pub(crate) fn run(mut opts: ReleaseOpts) -> Result<()> {
if opts.prepare {
apply_prepare_mode_to_skip(&mut opts.skip);
}
validate_strict_vs_allowlist(&opts)?;
let log = StageLogger::new(
"release",
Verbosity::from_flags(opts.quiet, opts.verbose, opts.debug),
);
git::check_git_available()?;
if opts.snapshot && opts.nightly {
anyhow::bail!("--snapshot and --nightly cannot be combined");
}
let config_path =
pipeline::find_config_with_logger(opts.config_override.as_deref(), Some(&log))?;
let mut config = pipeline::load_config(&config_path)?;
let workspace_skip = apply_workspace_overlay_for_opts(&mut config, &opts, &log)?;
helpers::infer_project_name(&mut config, &log);
helpers::auto_detect_github(&mut config, &log);
apply_release_meta_overrides(&mut config, &opts)?;
let all_known_crates: Vec<CrateConfig> = config.crate_universe().into_iter().cloned().collect();
let selected_sorted = recover_crate_selection(
resolve_selected_crates(&opts, &all_known_crates, &config, &log),
&opts,
&log,
)?;
if selected_sorted.is_empty() && selection_depends_on_head_tags(&opts) {
log.status("no release tags at HEAD — nothing to do");
return Ok(());
}
if opts.host_targets {
resolve_host_targets(&mut opts, &config, &selected_sorted, &log)?;
}
let skip_stages = compute_skip_stages(opts.skip.clone(), &workspace_skip, opts.snapshot);
let release_notes_path = read_release_notes_template(&opts)?;
let simulate_failure_publishers = resolve_simulate_failure(&mut opts.simulate_failure)?;
let runtime_nondeterministic_allowlist =
parse_allow_nondeterministic(&opts.allow_nondeterministic)?;
let project_root;
let mut ctx;
let setup_result;
{
let _setup = log.group("setup");
let log = log.with_stage("setup");
project_root = resolve_project_root(&config_path, Some(&log));
let (render_set, single_track) = crate::commands::changelog::resolve_changelog_render_set(
project_root
.as_deref()
.unwrap_or_else(|| std::path::Path::new(".")),
&config,
)?;
let changelog_aggregate_set = single_track.then_some(render_set);
let ctx_opts = build_context_options(
&opts,
skip_stages,
selected_sorted,
simulate_failure_publishers,
runtime_nondeterministic_allowlist,
project_root,
changelog_aggregate_set,
);
ctx = Context::new(config.clone(), ctx_opts);
install_verify_gate(&mut ctx);
pipeline::emit_config_advisories_filtered(&config, &log, |name| {
ctx.publisher_deselected(name)
});
helpers::resolve_scm_token_type(&mut ctx, &config);
ctx.populate_time_vars();
ctx.populate_runtime_vars();
ctx.populate_metadata_var()?;
ctx.template_vars_mut().set_bool("IsPrepare", opts.prepare);
enforce_dist_state(&config, &opts, &log)?;
helpers::setup_env(&mut ctx, &config, &log)?;
helpers::resolve_git_context(&mut ctx, &config, &log)?;
if ctx.is_nightly() {
apply_nightly_template_vars(&mut ctx, &config, &log)?;
}
if ctx.is_snapshot() {
apply_snapshot_template_vars(&mut ctx, &config, &log)?;
}
if should_run_preflight(
opts.snapshot,
opts.dry_run,
opts.split,
ctx.should_skip("preflight"),
) {
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 outcome = crate::commands::preflight::run_engine(&mut ctx, scope, &log)?;
if !outcome.ok() {
anyhow::bail!(outcome.failure_message());
}
}
setup_result =
run_setup_inside_always_bracket(&mut ctx, &config, &opts, release_notes_path, &log);
}
let outcome = match setup_result {
Err(err) => Err(err),
Ok(()) => {
let result = dispatch_release_modes(&mut ctx, &config, &opts, &log);
if let Err(ref err) = result {
on_error::fire_release_on_error(&ctx, err, &log);
}
result
}
};
helpers::finish_with_always_hooks(&ctx, outcome, &log)
}
fn run_setup_inside_always_bracket(
ctx: &mut Context,
config: &Config,
opts: &ReleaseOpts,
release_notes_path: Option<(PathBuf, String)>,
log: &StageLogger,
) -> Result<()> {
run_before_hooks(ctx, config, opts, log)?;
render_release_notes_tmpl(ctx, config, opts, release_notes_path, log)?;
enforce_dirty_repo_gate(ctx)?;
if !opts.publish_only {
helpers::write_effective_config(config, log)?;
}
if !opts.split
&& !opts.announce_only
&& let Some(ref milestones) = config.milestones
{
milestones::preflight_milestones(milestones, ctx, log)?;
}
Ok(())
}
pub(crate) fn dispatch_release_modes(
ctx: &mut Context,
config: &Config,
opts: &ReleaseOpts,
log: &StageLogger,
) -> Result<()> {
if opts.publish_only {
let dist = config.dist.clone();
let run_opts = publish_only::RunOpts {
dry_run: opts.dry_run,
};
if !opts.crate_names.is_empty() {
let with_subdir: Vec<String> = opts
.crate_names
.iter()
.filter(|name| publish_only::crate_subdir_has_manifest(&dist, name, log))
.cloned()
.collect();
if with_subdir.is_empty() {
return publish_only::run(ctx, config, log, run_opts);
}
if with_subdir.len() != opts.crate_names.len() {
let missing: Vec<&String> = opts
.crate_names
.iter()
.filter(|name| !with_subdir.iter().any(|s| s == *name))
.collect();
anyhow::bail!(
"publish-only --crate: {} of {} requested crate(s) have no \
preserved per-crate dist at {} (missing: {}). The remaining \
crates ({}) do. Refusing to silently publish only the subset \
before an irreversible publish — re-run with only the crates \
that were preserved, or re-run the preserve step so every \
requested crate has a dist/<crate>/ subdir.",
missing.len(),
opts.crate_names.len(),
dist.display(),
missing
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join(", "),
with_subdir.join(", "),
);
}
let all_known: Vec<CrateConfig> =
config.crate_universe().into_iter().cloned().collect();
let sorted = topo_sort_selected(&all_known, &with_subdir);
let order = if sorted.is_empty() {
with_subdir
} else {
sorted
};
return publish_only::run_per_crate(ctx, config, log, run_opts, dist, order);
}
match publish_only::detect_dist_layout(&dist, log)? {
publish_only::DistLayout::Flat => {
return publish_only::run(ctx, config, log, run_opts);
}
publish_only::DistLayout::PerCrate(subdirs) => {
let all_known: Vec<CrateConfig> =
config.crate_universe().into_iter().cloned().collect();
let sorted = topo_sort_selected(&all_known, &subdirs);
if sorted.is_empty()
&& let Some(ref ws_name) = opts.workspace
{
let ws_crates: Vec<&str> = all_known.iter().map(|c| c.name.as_str()).collect();
anyhow::bail!(
"publish-only --workspace {}: none of the preserved per-crate dist \
subdirs at {} ({}) belong to workspace '{}' (its crates: {}). \
Refusing to publish sibling crates under this workspace's \
env/signs/skip — check the workspace name, or point the run at \
the preserved dist produced for this workspace.",
ws_name,
dist.display(),
subdirs.join(", "),
ws_name,
if ws_crates.is_empty() {
"(none)".to_string()
} else {
ws_crates.join(", ")
},
);
}
let order = if sorted.is_empty() { subdirs } else { sorted };
return publish_only::run_per_crate(ctx, config, log, run_opts, dist, order);
}
publish_only::DistLayout::Ambiguous { crate_subdirs } => {
anyhow::bail!(
"publish-only: ambiguous dist layout at {} — found both a flat \
context.json at the root AND per-crate subdirectories ({}). \
Delete one or the other, or pass --crate <name> to select a \
specific crate.",
dist.display(),
crate_subdirs.join(", ")
);
}
}
}
if opts.announce_only {
return announce_only::run(ctx, config, log, opts.dry_run);
}
if opts.split {
return split::run_split(ctx, config, log);
}
if opts.merge {
return split::run_merge(ctx, config, log, opts.dry_run, None);
}
let p = pipeline::build_release_pipeline();
let result = p.run(ctx, log);
if result.is_ok() {
run_post_pipeline(ctx, config, opts.dry_run, RootAfterHooks::Fire, log)?;
}
if result.is_ok() {
gate_required_failures(ctx)?;
}
result
}