use std::collections::BTreeMap; use anyhow::bail; use crate::{ changelog, changelog::{write::Linkables, Section}, command::release::Options, traverse::{ self, dependency, dependency::{ManifestAdjustment, VersionAdjustment}, Dependency, }, utils::{tag_name, try_to_published_crate_and_new_version, will, Program}, version, version::BumpSpec,}; mod cargo;mod git;mod github;mod manifest; pub(crate) struct Context { base: crate::Context, changelog_links: Linkables,} impl Context { fn new( crate_names: Vec<String>, bump: BumpSpec, bump_dependencies: BumpSpec, changelog: bool, changelog_links: bool, ) -> anyhow::Result<Self> { let changelog_links = if changelog_links { crate::git::remote_url()? .map(|url| Linkables::AsLinks { repository_url: url.into(), }) .unwrap_or(Linkables::AsText) } else { Linkables::AsText }; Ok(Context { base: crate::Context::new(crate_names, changelog, bump, bump_dependencies)?, changelog_links, }) }} /// In order to try dealing with https://github.com/sunng87/cargo-release/issues/224 and also to make workspace/// releases more selective.pub fn release(opts: Options, crates: Vec<String>, bump: BumpSpec, bump_dependencies: BumpSpec) -> anyhow::Result<()> { if opts.dry_run_cargo_publish && !opts.dry_run { bail!("The --no-dry-run-cargo-publish flag is only effective without --execute") } let allow_changelog = if opts.changelog && opts.skip_tag { log::warn!("With --no-tag enabled, changelog generation will be disabled as it relies on tags to segment commit history."); false } else { opts.changelog }; if opts.update_crates_index { // Do this before creating our context to pick up a possibly newly fetched/created index. log::info!("Updating crates-io index",); crates_index::Index::new_cargo_default()?.update()?; } else if opts.bump_when_needed { log::warn!( "Consider running with --update-crates-index to assure bumping on demand uses the latest information" ); } let ctx = Context::new(crates, bump, bump_dependencies, allow_changelog, opts.changelog_links)?; if !ctx.base.crates_index.exists() { log::warn!("Crates.io index doesn't exist. Consider using --update-crates-index to help determining if release versions are published already"); } release_depth_first(ctx, opts)?; Ok(())} impl From<Options> for crate::traverse::Options { fn from(v: Options) -> Self { Self { allow_auto_publish_of_stable_crates: v.allow_auto_publish_of_stable_crates, bump_when_needed: v.bump_when_needed, isolate_dependencies_from_breaking_changes: v.isolate_dependencies_from_breaking_changes, traverse_graph: v.dependencies, } }} fn release_depth_first(ctx: Context, opts: Options) -> anyhow::Result<()> { let crates = { crate::traverse::dependencies(&ctx.base, opts.into()) .and_then(|crates| assure_crates_index_is_uptodate(crates, &ctx.base, opts.into())) .and_then(|crates| { present_and_validate_dependencies(&crates, &ctx, opts.verbose, opts.dry_run).map(|_| crates) })? }; assure_working_tree_is_unchanged(opts)?; perform_release(&ctx, opts, &crates)?; Ok(())} fn assure_crates_index_is_uptodate<'meta>( crates: Vec<Dependency<'meta>>, ctx: &'meta crate::Context, opts: crate::traverse::Options,) -> anyhow::Result<Vec<Dependency<'meta>>> { if let Some(dep) = crates .iter() .filter_map(|d| d.mode.version_adjustment_bump().map(|b| (d, b))) .find_map(|(d, b)| { b.latest_release .as_ref() .and_then(|lr| (lr >= &b.desired_release).then(|| d)) }) { let mut index = crate::crates_index::Index::new_cargo_default()?; if index.exists() { log::warn!("Crate '{}' computed version not greater than the current package version. Updating crates index to assure correct results.", dep.package.name); index.update()?; return crate::traverse::dependencies(ctx, opts); } } Ok(crates)} fn present_and_validate_dependencies( crates: &[traverse::Dependency<'_>], ctx: &Context, verbose: bool, dry_run: bool,) -> anyhow::Result<()> { use dependency::Kind; let all_skipped: Vec<_> = crates .iter() .filter_map(|dep| match &dep.mode { dependency::Mode::NotForPublishing { reason, adjustment } => { Some((dep.package.name.as_str(), adjustment.is_some(), *reason)) } _ => None, }) .collect(); let mut num_refused = 0; for (refused_crate, has_adjustment, _) in all_skipped .iter() .filter(|(name, _, _)| ctx.base.crate_names.iter().any(|n| n == *name)) { num_refused += 1; log::warn!( "Refused to publish '{}' as {}.", refused_crate, has_adjustment .then(|| "only a manifest change is needed") .unwrap_or("as it didn't change") ); } let no_requested_crate_will_publish = num_refused == ctx.base.crate_names.len(); let no_crate_being_published_message = "No provided crate is actually eligible for publishing."; if no_requested_crate_will_publish && !verbose { bail!( "{} Use --verbose to see the release plan nonetheless.", no_crate_being_published_message ) } let skipped = all_skipped .iter() .filter_map(|(name, has_adjustment, reason)| (!has_adjustment).then(|| (*name, reason))) .collect::<Vec<_>>(); if !skipped.is_empty() { let skipped_len = skipped.len(); let mut crates_by_reason: Vec<_> = skipped .into_iter() .fold(BTreeMap::default(), |mut acc, (name, reason)| { acc.entry(*reason).or_insert_with(Vec::new).push(name); acc }) .into_iter() .collect(); crates_by_reason.sort_by_key(|(k, _)| *k); log::info!( "Will not publish or alter {} dependent crate{}: {}", skipped_len, (skipped_len != 1).then(|| "s").unwrap_or(""), crates_by_reason .into_iter() .map(|(key, names)| format!( "{} = {}", key, names.iter().map(|n| format!("'{}'", n)).collect::<Vec<_>>().join(", ") )) .collect::<Vec<_>>() .join(", ") ); } if all_skipped.len() == crates.len() { bail!("There is no crate eligible for publishing."); } let mut error = false; for dep in crates { let (bump_spec, kind) = match dep.kind { Kind::UserSelection => (ctx.base.bump, "provided"), Kind::DependencyOrDependentOfUserSelection => (ctx.base.bump_dependencies, "dependent"), }; match &dep.mode { dependency::Mode::ToBePublished { adjustment } => { let (bump, breaking_dependencies) = match adjustment { VersionAdjustment::Breakage { bump, causing_dependency_names, .. } => (bump, Some(causing_dependency_names)), VersionAdjustment::Changed { bump, .. } => (bump, None), }; if let Some(latest_release) = bump .latest_release .as_ref() .and_then(|lr| (*lr >= bump.desired_release).then(|| lr)) { let bump_flag = match dep.kind { dependency::Kind::UserSelection => "--bump <level>", dependency::Kind::DependencyOrDependentOfUserSelection => "--bump-dependencies <level>", }; if bump.desired_release == bump.package_version { log::warn!( "'{}' is unchanged. Consider using {} along with --no-bump-on-demand to force a version change.", dep.package.name, bump_flag ); } else { log::warn!( "Latest published version of '{}' is {}, the new version is {}. Consider using {} or update the index with --update-crates-index.", dep.package.name, latest_release, bump.desired_release, bump_flag ); } error = true; } if bump.next_release > dep.package.version { log::info!( "{} {}-bump {} package '{}' from {} to {} for publishing{}{}{}", will(dry_run), bump_spec, kind, dep.package.name, dep.package.version, bump.next_release, bump.latest_release .as_ref() .and_then(|latest_release| { (dep.package.version != *latest_release) .then(|| format!(", {} on crates.io", latest_release)) }) .unwrap_or_default(), breaking_dependencies .map(|causes| format!( ", for SAFETY due to breaking package{} {}", if causes.len() == 1 { "" } else { "s" }, causes.iter().map(|n| format!("'{}'", n)).collect::<Vec<_>>().join(", ") )) .unwrap_or_default(), (bump.next_release != bump.desired_release) .then(|| format!(", ignoring computed version {}", bump.desired_release)) .unwrap_or_default(), ); } else { log::info!( "Manifest version of {} package '{}' at {} is sufficient{}, ignoring computed version {}", kind, dep.package.name, dep.package.version, bump.latest_release .as_ref() .map(|latest_release| format!(" to succeed latest released version {}", latest_release)) .unwrap_or_else(|| ", creating a new release 🎉".into()), bump.desired_release ); }; } dependency::Mode::NotForPublishing { .. } => {} } } { let affected_crates_by_cause = crates .iter() .filter_map(|dep| match &dep.mode { dependency::Mode::NotForPublishing { adjustment: Some(ManifestAdjustment::Version(VersionAdjustment::Breakage { bump, causing_dependency_names, .. })), .. } | dependency::Mode::ToBePublished { adjustment: VersionAdjustment::Breakage { bump, causing_dependency_names, .. }, } => Some((dep, bump, causing_dependency_names)), _ => None, }) .fold( Vec::new(), |mut acc: Vec<(&str, Vec<(&str, &version::Bump)>)>, (dep, bump, causing_names)| { for name in causing_names { match acc.iter_mut().find(|(k, _v)| k == name) { Some((_k, deps)) => deps.push((dep.package.name.as_str(), bump)), None => acc.push((name, vec![(dep.package.name.as_str(), bump)])), } } acc }, ); for (cause, deps_and_bumps) in affected_crates_by_cause { let plural_s = (deps_and_bumps.len() != 1).then(|| "s").unwrap_or(""); log::info!( "{} adjust {} manifest version{} due to breaking change in '{}': {}", will(dry_run), deps_and_bumps.len(), plural_s, cause, deps_and_bumps .into_iter() .map(|(dep_name, bump)| format!("'{}' {} ➡ {}", dep_name, bump.package_version, bump.next_release)) .collect::<Vec<_>>() .join(", ") ); } } { let crate_names_for_manifest_updates = crates .iter() .filter_map(|d| { matches!( d.mode, dependency::Mode::NotForPublishing { adjustment: Some(ManifestAdjustment::DueToDependencyChange), .. } ) .then(|| d.package.name.as_str()) }) .collect::<Vec<_>>(); if !crate_names_for_manifest_updates.is_empty() { let plural_s = (crate_names_for_manifest_updates.len() > 1) .then(|| "s") .unwrap_or_default(); log::info!( "{} adjust version constraints in manifest{} of {} package{} as direct dependencies are changing: {}", will(dry_run), plural_s, crate_names_for_manifest_updates.len(), plural_s, crate_names_for_manifest_updates.join(", ") ); } } if error { bail!("Aborting due to previous error(s)"); } else { if no_requested_crate_will_publish { bail!(no_crate_being_published_message) } Ok(()) }} fn assure_working_tree_is_unchanged(options: Options) -> anyhow::Result<()> { if !options.allow_dirty { if let Err(err) = crate::git::assure_clean_working_tree() { if options.dry_run { log::warn!("The working tree has changes which will prevent a release with --execute unless --allow-dirty is also specified. The latter isn't recommended.") } else { return Err(err); } } } Ok(())} fn perform_release(ctx: &Context, options: Options, crates: &[traverse::Dependency<'_>]) -> anyhow::Result<()> { let manifest::Outcome { commit_id, section_by_package: release_section_by_publishee, } = manifest::edit_version_and_fixup_dependent_crates_and_handle_changelog(crates, options, ctx)?; let should_publish_to_github = options.allow_changelog_github_release && if Program::named("gh").found { true } else { log::warn!("To create github releases, please install the 'gh' program and try again"); false }; let mut tag_names = Vec::new(); let mut successful_publishees_and_version = Vec::<(&cargo_metadata::Package, &semver::Version)>::new(); let mut publish_err = None; let prevent_default_members = ctx.base.meta.workspace_members.len() > 1; for (publishee, new_version) in crates.iter().filter_map(try_to_published_crate_and_new_version) { if let Some((crate_, version)) = successful_publishees_and_version.last() { if let Err(err) = wait_for_release(*crate_, version, options) { log::warn!( "Failed to wait for crates-index update - trying to publish '{} v{}' anyway: {}.", publishee.name, new_version, err ); } } if let Err(err) = cargo::publish_crate(publishee, prevent_default_members, options) { publish_err = Some(err); break; } successful_publishees_and_version.push((publishee, new_version)); if let Some(tag_name) = git::create_version_tag( publishee, new_version, commit_id, release_section_by_publishee .get(&publishee.name.as_str()) .and_then(|s| section_to_string(s, WriteMode::Tag)), &ctx.base, options, )? { tag_names.push(tag_name); } } git::push_tags_and_head(&tag_names, options)?; if should_publish_to_github { for (publishee, new_version) in successful_publishees_and_version { release_section_by_publishee .get(&publishee.name.as_str()) .and_then(|s| section_to_string(s, WriteMode::GitHubRelease)) .map(|release_notes| github::create_release(publishee, new_version, &release_notes, options, &ctx.base)) .transpose()?; } } publish_err.map(Err).unwrap_or(Ok(()))} fn wait_for_release( crate_: &cargo_metadata::Package, crate_version: &semver::Version, Options { dry_run, dry_run_cargo_publish, skip_publish, .. }: Options,) -> anyhow::Result<()> { use anyhow::Context; if skip_publish || dry_run || dry_run_cargo_publish { return Ok(()); } let timeout = std::time::Duration::from_secs(60); let start = std::time::Instant::now(); let sleep_time = std::time::Duration::from_secs(1); let crate_version = crate_version.to_string(); log::info!("Waiting for '{} v{}' to arrive in index…", crate_.name, crate_version); let mut crates_index = crates_index::Index::new_cargo_default()?; let mut attempt = 0; while start.elapsed() < timeout { attempt += 1; log::trace!("Updating crates index…"); crates_index.update()?; let crate_ = crates_index.crate_(&crate_.name).with_context(|| { format!( "Couldn't find crate '{}' in index anymore - unexpected and fatal", crate_.name ) })?; if crate_ .versions() .iter() .rev()Lines 1–500 of 530. Load more
use std::collections::BTreeMap; use anyhow::bail; use crate::{ changelog, changelog::{write::Linkables, Section}, command::release::Options, traverse::{ self, dependency, dependency::{ManifestAdjustment, VersionAdjustment}, Dependency, }, utils::{tag_name, try_to_published_crate_and_new_version, will, Program}, version, version::BumpSpec, }; mod cargo; mod git; mod github; mod manifest; pub(crate) struct Context { base: crate::Context, changelog_links: Linkables, } impl Context { fn new( crate_names: Vec<String>, bump: BumpSpec, bump_dependencies: BumpSpec, changelog: bool, changelog_links: bool, ) -> anyhow::Result<Self> { let changelog_links = if changelog_links { crate::git::remote_url()? .map(|url| Linkables::AsLinks { repository_url: url.into(), }) .unwrap_or(Linkables::AsText) } else { Linkables::AsText }; Ok(Context { base: crate::Context::new(crate_names, changelog, bump, bump_dependencies)?, changelog_links, }) } } /// In order to try dealing with https://github.com/sunng87/cargo-release/issues/224 and also to make workspace /// releases more selective. pub fn release(opts: Options, crates: Vec<String>, bump: BumpSpec, bump_dependencies: BumpSpec) -> anyhow::Result<()> { if opts.dry_run_cargo_publish && !opts.dry_run { bail!("The --no-dry-run-cargo-publish flag is only effective without --execute") } let allow_changelog = if opts.changelog && opts.skip_tag { log::warn!("With --no-tag enabled, changelog generation will be disabled as it relies on tags to segment commit history."); false } else { opts.changelog }; if opts.update_crates_index { // Do this before creating our context to pick up a possibly newly fetched/created index. log::info!("Updating crates-io index",); crates_index::Index::new_cargo_default()?.update()?; } else if opts.bump_when_needed { log::warn!( "Consider running with --update-crates-index to assure bumping on demand uses the latest information" ); } let ctx = Context::new(crates, bump, bump_dependencies, allow_changelog, opts.changelog_links)?; if !ctx.base.crates_index.exists() { log::warn!("Crates.io index doesn't exist. Consider using --update-crates-index to help determining if release versions are published already"); } release_depth_first(ctx, opts)?; Ok(()) } impl From<Options> for crate::traverse::Options { fn from(v: Options) -> Self { Self { allow_auto_publish_of_stable_crates: v.allow_auto_publish_of_stable_crates, bump_when_needed: v.bump_when_needed, isolate_dependencies_from_breaking_changes: v.isolate_dependencies_from_breaking_changes, traverse_graph: v.dependencies, } } } fn release_depth_first(ctx: Context, opts: Options) -> anyhow::Result<()> { let crates = { crate::traverse::dependencies(&ctx.base, opts.into()) .and_then(|crates| assure_crates_index_is_uptodate(crates, &ctx.base, opts.into())) .and_then(|crates| { present_and_validate_dependencies(&crates, &ctx, opts.verbose, opts.dry_run).map(|_| crates) })? }; assure_working_tree_is_unchanged(opts)?; perform_release(&ctx, opts, &crates)?; Ok(()) } fn assure_crates_index_is_uptodate<'meta>( crates: Vec<Dependency<'meta>>, ctx: &'meta crate::Context, opts: crate::traverse::Options, ) -> anyhow::Result<Vec<Dependency<'meta>>> { if let Some(dep) = crates .iter() .filter_map(|d| d.mode.version_adjustment_bump().map(|b| (d, b))) .find_map(|(d, b)| { b.latest_release .as_ref() .and_then(|lr| (lr >= &b.desired_release).then(|| d)) }) { let mut index = crate::crates_index::Index::new_cargo_default()?; if index.exists() { log::warn!("Crate '{}' computed version not greater than the current package version. Updating crates index to assure correct results.", dep.package.name); index.update()?; return crate::traverse::dependencies(ctx, opts); } } Ok(crates) } fn present_and_validate_dependencies( crates: &[traverse::Dependency<'_>], ctx: &Context, verbose: bool, dry_run: bool, ) -> anyhow::Result<()> { use dependency::Kind; let all_skipped: Vec<_> = crates .iter() .filter_map(|dep| match &dep.mode { dependency::Mode::NotForPublishing { reason, adjustment } => { Some((dep.package.name.as_str(), adjustment.is_some(), *reason)) } _ => None, }) .collect(); let mut num_refused = 0; for (refused_crate, has_adjustment, _) in all_skipped .iter() .filter(|(name, _, _)| ctx.base.crate_names.iter().any(|n| n == *name)) { num_refused += 1; log::warn!( "Refused to publish '{}' as {}.", refused_crate, has_adjustment .then(|| "only a manifest change is needed") .unwrap_or("as it didn't change") ); } let no_requested_crate_will_publish = num_refused == ctx.base.crate_names.len(); let no_crate_being_published_message = "No provided crate is actually eligible for publishing."; if no_requested_crate_will_publish && !verbose { bail!( "{} Use --verbose to see the release plan nonetheless.", no_crate_being_published_message ) } let skipped = all_skipped .iter() .filter_map(|(name, has_adjustment, reason)| (!has_adjustment).then(|| (*name, reason))) .collect::<Vec<_>>(); if !skipped.is_empty() { let skipped_len = skipped.len(); let mut crates_by_reason: Vec<_> = skipped .into_iter() .fold(BTreeMap::default(), |mut acc, (name, reason)| { acc.entry(*reason).or_insert_with(Vec::new).push(name); acc }) .into_iter() .collect(); crates_by_reason.sort_by_key(|(k, _)| *k); log::info!( "Will not publish or alter {} dependent crate{}: {}", skipped_len, (skipped_len != 1).then(|| "s").unwrap_or(""), crates_by_reason .into_iter() .map(|(key, names)| format!( "{} = {}", key, names.iter().map(|n| format!("'{}'", n)).collect::<Vec<_>>().join(", ") )) .collect::<Vec<_>>() .join(", ") ); } if all_skipped.len() == crates.len() { bail!("There is no crate eligible for publishing."); } let mut error = false; for dep in crates { let (bump_spec, kind) = match dep.kind { Kind::UserSelection => (ctx.base.bump, "provided"), Kind::DependencyOrDependentOfUserSelection => (ctx.base.bump_dependencies, "dependent"), }; match &dep.mode { dependency::Mode::ToBePublished { adjustment } => { let (bump, breaking_dependencies) = match adjustment { VersionAdjustment::Breakage { bump, causing_dependency_names, .. } => (bump, Some(causing_dependency_names)), VersionAdjustment::Changed { bump, .. } => (bump, None), }; if let Some(latest_release) = bump .latest_release .as_ref() .and_then(|lr| (*lr >= bump.desired_release).then(|| lr)) { let bump_flag = match dep.kind { dependency::Kind::UserSelection => "--bump <level>", dependency::Kind::DependencyOrDependentOfUserSelection => "--bump-dependencies <level>", }; if bump.desired_release == bump.package_version { log::warn!( "'{}' is unchanged. Consider using {} along with --no-bump-on-demand to force a version change.", dep.package.name, bump_flag ); } else { log::warn!( "Latest published version of '{}' is {}, the new version is {}. Consider using {} or update the index with --update-crates-index.", dep.package.name, latest_release, bump.desired_release, bump_flag ); } error = true; } if bump.next_release > dep.package.version { log::info!( "{} {}-bump {} package '{}' from {} to {} for publishing{}{}{}", will(dry_run), bump_spec, kind, dep.package.name, dep.package.version, bump.next_release, bump.latest_release .as_ref() .and_then(|latest_release| { (dep.package.version != *latest_release) .then(|| format!(", {} on crates.io", latest_release)) }) .unwrap_or_default(), breaking_dependencies .map(|causes| format!( ", for SAFETY due to breaking package{} {}", if causes.len() == 1 { "" } else { "s" }, causes.iter().map(|n| format!("'{}'", n)).collect::<Vec<_>>().join(", ") )) .unwrap_or_default(), (bump.next_release != bump.desired_release) .then(|| format!(", ignoring computed version {}", bump.desired_release)) .unwrap_or_default(), ); } else { log::info!( "Manifest version of {} package '{}' at {} is sufficient{}, ignoring computed version {}", kind, dep.package.name, dep.package.version, bump.latest_release .as_ref() .map(|latest_release| format!(" to succeed latest released version {}", latest_release)) .unwrap_or_else(|| ", creating a new release 🎉".into()), bump.desired_release ); }; } dependency::Mode::NotForPublishing { .. } => {} } } { let affected_crates_by_cause = crates .iter() .filter_map(|dep| match &dep.mode { dependency::Mode::NotForPublishing { adjustment: Some(ManifestAdjustment::Version(VersionAdjustment::Breakage { bump, causing_dependency_names, .. })), .. } | dependency::Mode::ToBePublished { adjustment: VersionAdjustment::Breakage { bump, causing_dependency_names, .. }, } => Some((dep, bump, causing_dependency_names)), _ => None, }) .fold( Vec::new(), |mut acc: Vec<(&str, Vec<(&str, &version::Bump)>)>, (dep, bump, causing_names)| { for name in causing_names { match acc.iter_mut().find(|(k, _v)| k == name) { Some((_k, deps)) => deps.push((dep.package.name.as_str(), bump)), None => acc.push((name, vec![(dep.package.name.as_str(), bump)])), } } acc }, ); for (cause, deps_and_bumps) in affected_crates_by_cause { let plural_s = (deps_and_bumps.len() != 1).then(|| "s").unwrap_or(""); log::info!( "{} adjust {} manifest version{} due to breaking change in '{}': {}", will(dry_run), deps_and_bumps.len(), plural_s, cause, deps_and_bumps .into_iter() .map(|(dep_name, bump)| format!("'{}' {} ➡ {}", dep_name, bump.package_version, bump.next_release)) .collect::<Vec<_>>() .join(", ") ); } } { let crate_names_for_manifest_updates = crates .iter() .filter_map(|d| { matches!( d.mode, dependency::Mode::NotForPublishing { adjustment: Some(ManifestAdjustment::DueToDependencyChange), .. } ) .then(|| d.package.name.as_str()) }) .collect::<Vec<_>>(); if !crate_names_for_manifest_updates.is_empty() { let plural_s = (crate_names_for_manifest_updates.len() > 1) .then(|| "s") .unwrap_or_default(); log::info!( "{} adjust version constraints in manifest{} of {} package{} as direct dependencies are changing: {}", will(dry_run), plural_s, crate_names_for_manifest_updates.len(), plural_s, crate_names_for_manifest_updates.join(", ") ); } } if error { bail!("Aborting due to previous error(s)"); } else { if no_requested_crate_will_publish { bail!(no_crate_being_published_message) } Ok(()) } } fn assure_working_tree_is_unchanged(options: Options) -> anyhow::Result<()> { if !options.allow_dirty { if let Err(err) = crate::git::assure_clean_working_tree() { if options.dry_run { log::warn!("The working tree has changes which will prevent a release with --execute unless --allow-dirty is also specified. The latter isn't recommended.") } else { return Err(err); } } } Ok(()) } fn perform_release(ctx: &Context, options: Options, crates: &[traverse::Dependency<'_>]) -> anyhow::Result<()> { let manifest::Outcome { commit_id, section_by_package: release_section_by_publishee, } = manifest::edit_version_and_fixup_dependent_crates_and_handle_changelog(crates, options, ctx)?; let should_publish_to_github = options.allow_changelog_github_release && if Program::named("gh").found { true } else { log::warn!("To create github releases, please install the 'gh' program and try again"); false }; let mut tag_names = Vec::new(); let mut successful_publishees_and_version = Vec::<(&cargo_metadata::Package, &semver::Version)>::new(); let mut publish_err = None; let prevent_default_members = ctx.base.meta.workspace_members.len() > 1; for (publishee, new_version) in crates.iter().filter_map(try_to_published_crate_and_new_version) { if let Some((crate_, version)) = successful_publishees_and_version.last() { if let Err(err) = wait_for_release(*crate_, version, options) { log::warn!( "Failed to wait for crates-index update - trying to publish '{} v{}' anyway: {}.", publishee.name, new_version, err ); } } if let Err(err) = cargo::publish_crate(publishee, prevent_default_members, options) { publish_err = Some(err); break; } successful_publishees_and_version.push((publishee, new_version)); if let Some(tag_name) = git::create_version_tag( publishee, new_version, commit_id, release_section_by_publishee .get(&publishee.name.as_str()) .and_then(|s| section_to_string(s, WriteMode::Tag)), &ctx.base, options, )? { tag_names.push(tag_name); } } git::push_tags_and_head(&tag_names, options)?; if should_publish_to_github { for (publishee, new_version) in successful_publishees_and_version { release_section_by_publishee .get(&publishee.name.as_str()) .and_then(|s| section_to_string(s, WriteMode::GitHubRelease)) .map(|release_notes| github::create_release(publishee, new_version, &release_notes, options, &ctx.base)) .transpose()?; } } publish_err.map(Err).unwrap_or(Ok(())) } fn wait_for_release( crate_: &cargo_metadata::Package, crate_version: &semver::Version, Options { dry_run, dry_run_cargo_publish, skip_publish, .. }: Options, ) -> anyhow::Result<()> { use anyhow::Context; if skip_publish || dry_run || dry_run_cargo_publish { return Ok(()); } let timeout = std::time::Duration::from_secs(60); let start = std::time::Instant::now(); let sleep_time = std::time::Duration::from_secs(1); let crate_version = crate_version.to_string(); log::info!("Waiting for '{} v{}' to arrive in index…", crate_.name, crate_version); let mut crates_index = crates_index::Index::new_cargo_default()?; let mut attempt = 0; while start.elapsed() < timeout { attempt += 1; log::trace!("Updating crates index…"); crates_index.update()?; let crate_ = crates_index.crate_(&crate_.name).with_context(|| { format!( "Couldn't find crate '{}' in index anymore - unexpected and fatal", crate_.name ) })?; if crate_ .versions() .iter() .rev()