Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion xtask/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ reqwest = { version = "0.12.12", features = [
# This pulls a gazillion crates - don't include it by default
cargo-semver-checks = { version = "0.46.0", optional = true }

# Reads the crates.io index, including the yanked flag that cargo hides.
tame-index = { version = "0.25", features = ["sparse"], optional = true }

flate2 = { version = "1.1.1", optional = true }
temp-file = { version = "0.1.9", optional = true }

Expand All @@ -59,7 +62,7 @@ tempfile = "3"
deploy-docs = ["dep:reqwest", "dep:kuchikiki"]
preview-docs = ["dep:opener", "dep:rocket"]
semver-checks = [ "dep:cargo-semver-checks", "dep:flate2", "dep:temp-file", "dep:regex" ]
release = ["semver-checks", "dep:opener", "dep:regex"]
release = ["semver-checks", "dep:opener", "dep:regex", "dep:tame-index"]
report = ["dep:regex"]
rel-check = ["dep:regex"]
mcp = ["dep:inventory", "dep:rmcp", "dep:schemars", "dep:tokio", "dep:xtask-mcp-macros"]
Expand Down
2 changes: 2 additions & 0 deletions xtask/src/commands/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ pub mod post_release;
pub mod publish;
#[cfg(feature = "release")]
pub mod publish_plan;
#[cfg(feature = "release")]
pub mod registry;
pub mod semver_check;
pub mod tag_releases;

Expand Down
63 changes: 45 additions & 18 deletions xtask/src/commands/release/bump_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,16 @@ pub fn bump_version(workspace: &Path, args: BumpVersionArgs) -> Result<()> {
// Bump the version for each given package:
for package in args.packages {
let mut package = CargoToml::new(workspace, package)?;
update_package(&mut package, &bump, false, false)?;
let new_version = do_version_bump(&package.package_version(), &bump)
.with_context(|| format!("Failed to bump version of {}", package.package))?;
update_package(&mut package, &new_version, false, false)?;
}

Ok(())
}

/// Update the specified package by bumping its version, updating its changelog,
/// Move the specified package to `new_version`, updating its changelog and
/// version placeholders along the way.
///
/// `skip_dependent_rewrites` skips rewriting intra-workspace path-dep version
/// requirements on sibling crates. Set this on backport patch releases: those
Expand All @@ -122,16 +125,16 @@ pub fn bump_version(workspace: &Path, args: BumpVersionArgs) -> Result<()> {
/// them anyway is pure churn.
pub fn update_package(
package: &mut CargoToml,
version: &VersionBump,
new_version: &semver::Version,
dry_run: bool,
skip_dependent_rewrites: bool,
) -> Result<semver::Version> {
) -> Result<()> {
check_crate_before_bumping(package)?;
let new_version = bump_crate_version(package, version, dry_run, skip_dependent_rewrites)?;
finalize_changelog(package, &new_version, dry_run)?;
finalize_placeholders(package, &new_version, dry_run)?;
bump_crate_version(package, new_version, dry_run, skip_dependent_rewrites)?;
finalize_changelog(package, new_version, dry_run)?;
finalize_placeholders(package, new_version, dry_run)?;

Ok(new_version)
Ok(())
}

fn check_crate_before_bumping(manifest: &mut CargoToml) -> Result<()> {
Expand Down Expand Up @@ -230,26 +233,24 @@ fn check_dependency_before_bumping(item: &Item) -> Result<()> {
Ok(())
}

/// Bump the version of the specified package by the specified amount.
/// Write the given version into the package's manifest and into the manifests
/// of every workspace crate that depends on it.
fn bump_crate_version(
bumped_package: &mut CargoToml,
amount: &VersionBump,
version: &semver::Version,
dry_run: bool,
skip_dependent_rewrites: bool,
) -> Result<semver::Version> {
) -> Result<()> {
let prev_version = bumped_package.package_version();

let version = do_version_bump(&prev_version, amount)
.with_context(|| format!("Failed to bump version of {}", bumped_package.package))?;

if dry_run {
log::info!(
"Dry run: would bump {} version to {version}",
bumped_package.package,
);
} else {
log::info!("Update {} to {version}", bumped_package.package);
bumped_package.set_version(&version);
bumped_package.set_version(version);
bumped_package.save()?;
}

Expand All @@ -258,7 +259,7 @@ fn bump_crate_version(
" Skipping intra-workspace dependent rewrites for {}",
bumped_package.package,
);
return Ok(version);
return Ok(());
}

let package_name = bumped_package.package.to_string();
Expand All @@ -284,7 +285,7 @@ fn bump_crate_version(

for dependent in tomls {
let mut dependent = dependent?;
if dependent.change_version_of_dependency(&package_name, &version) {
if dependent.change_version_of_dependency(&package_name, version) {
if dry_run {
log::info!(
" Dry run: would update {} in {}: ({prev_version} -> {version})",
Expand All @@ -301,7 +302,7 @@ fn bump_crate_version(
}
}

Ok(version)
Ok(())
}

/// Bump only the base version (`major.minor.patch`).
Expand Down Expand Up @@ -597,6 +598,32 @@ mod tests {
}
}

/// The version handed to `update_package` must reach the manifest verbatim.
#[test]
fn update_package_writes_the_version_it_is_given() {
let workspace = tempfile::tempdir().unwrap();
let package_dir = workspace.path().join(Package::EspSync.to_string());
fs::create_dir(&package_dir).unwrap();
fs::write(
package_dir.join("Cargo.toml"),
"[package]\nname = \"esp-sync\"\nversion = \"0.1.1\"\n",
)
.unwrap();

let mut manifest = CargoToml::new(workspace.path(), Package::EspSync).unwrap();

// No VersionBump can reach 0.2.1 from 0.1.1 — Minor gives 0.2.0, Patch
// gives 0.1.2 — so this only passes if the version travels as data.
let resolved = semver::Version::parse("0.2.1").unwrap();
update_package(&mut manifest, &resolved, false, true).unwrap();

let written = fs::read_to_string(package_dir.join("Cargo.toml")).unwrap();
assert!(
written.contains(r#"version = "0.2.1""#),
"manifest did not receive the resolved version:\n{written}"
);
}

#[test]
fn test_rejected_dependencies() {
let toml = r#"
Expand Down
35 changes: 32 additions & 3 deletions xtask/src/commands/release/execute_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@ use crate::{
commands::{
VersionBump,
checker::generate_baseline,
release::plan::{PackagePlan, Plan},
do_version_bump,
release::{
plan::{PackagePlan, Plan},
registry::RegistrySnapshot,
},
update_package,
},
git::{current_branch, ensure_workspace_clean, get_remote_name_for},
Expand All @@ -28,6 +32,13 @@ pub struct ApplyPlanArgs {
/// Instead of opening the pull request, just print base URL and body.
#[arg(long)]
manual_pull_request: bool,

/// Do not ask crates.io which version numbers are already taken.
///
/// The check needs network access. Skipping it means the release may be
/// prepared with a version that `cargo publish` will reject at the very end.
#[arg(long)]
skip_registry_check: bool,
}

/// Execute the release plan by making code changes, committing them to a new
Expand Down Expand Up @@ -107,6 +118,13 @@ pub fn execute_plan(workspace: &Path, args: ApplyPlanArgs) -> Result<()> {
println!("Dry run: would merge PR changelog entries into CHANGELOG.md / MIGRATING-*.md");
}

let snapshot = if args.skip_registry_check {
println!("Skipping the crates.io version check.");
RegistrySnapshot::skipped()
} else {
RegistrySnapshot::fetch(plan.packages.iter().map(|step| step.package))?
};

// Make code changes, reusing the manifests validated above. Packages that
// were already at their target version are stored as `None` and skipped.
let skip_dependent_rewrites = plan.backport.is_some();
Expand All @@ -115,9 +133,20 @@ pub fn execute_plan(workspace: &Path, args: ApplyPlanArgs) -> Result<()> {
continue;
};

let new_version = update_package(
let planned = do_version_bump(&package.package_version(), &step.bump)
.with_context(|| format!("Failed to bump version of {}", step.package))?;
let new_version = snapshot.next_free_version(step.package, &planned, &step.bump)?;

if new_version != planned {
println!(
"{}: {planned} is reserved on crates.io, releasing {new_version} instead.",
step.package
);
}

update_package(
&mut package,
&step.bump,
&new_version,
!args.no_dry_run,
skip_dependent_rewrites,
)?;
Expand Down
24 changes: 22 additions & 2 deletions xtask/src/commands/release/plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use crate::{
VersionBump,
checker::min_package_update,
do_version_bump,
release::changelog_preview,
release::{changelog_preview, registry},
},
git::{BackportInfo, current_branch, parse_backport_branch},
metadata::Chip,
Expand All @@ -29,6 +29,13 @@ pub struct PlanArgs {
#[arg(long)]
allow_non_main: bool,

/// Do not ask crates.io which version numbers are already taken.
///
/// The check needs network access. Skipping it means the plan may pick a
/// version that `cargo publish` will reject at the very end of the release.
#[arg(long)]
skip_registry_check: bool,

/// The packages to be released.
#[arg(value_enum, default_values_t = Package::iter())]
packages: Vec<Package>,
Expand Down Expand Up @@ -252,7 +259,7 @@ pub fn plan(workspace: &Path, args: PlanArgs) -> Result<()> {
// after tweaks keeps targeting the same release branch.
let slug = read_existing_slug(&plan_path)?.unwrap_or_else(generate_slug);

let plan = Plan {
let mut plan = Plan {
base: current_branch,
slug,
backport: backport.clone(),
Expand Down Expand Up @@ -308,6 +315,19 @@ pub fn plan(workspace: &Path, args: PlanArgs) -> Result<()> {
.collect(),
};

if args.skip_registry_check {
println!("Skipping the crates.io version check.");
} else {
let snapshot =
registry::RegistrySnapshot::fetch(plan.packages.iter().map(|step| step.package))?;

for step in plan.packages.iter_mut() {
step.new_version =
snapshot.next_free_version(step.package, &step.new_version, &step.bump)?;
step.tag_name = step.package.tag(&step.new_version);
}
}

log::debug!("Writing release plan to {}", plan_path.display());

let mut plan_header = String::from(
Expand Down
20 changes: 19 additions & 1 deletion xtask/src/commands/release/publish_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ use clap::Args;

use crate::{
cargo::{CargoArgsBuilder, CargoToml},
commands::Plan,
commands::{
Plan,
release::registry::{RegistrySnapshot, Slot},
},
git::{current_branch, ensure_workspace_clean, get_remote_name_for},
};

Expand Down Expand Up @@ -47,6 +50,8 @@ pub fn publish_plan(workspace: &Path, args: PublishPlanArgs) -> Result<()> {
})
.collect::<Result<Vec<_>>>()?;

let snapshot = RegistrySnapshot::fetch(plan.packages.iter().map(|step| step.package))?;

// Check that all packages are updated and ready to go. This is meant to prevent
// publishing unupdated packages.
for (step, toml) in plan.packages.iter().zip(tomls.iter()) {
Expand All @@ -67,6 +72,19 @@ pub fn publish_plan(workspace: &Path, args: PublishPlanArgs) -> Result<()> {
step.package
);
}

let slot = snapshot.slot(step.package, &step.new_version);
if slot != Slot::Free {
let yanked = match slot {
Slot::Yanked => " and yanked",
_ => "",
};
bail!(
"{} {} is already published on crates.io{yanked}.",
step.package,
step.new_version,
);
}
}

// Actually publish the packages.
Expand Down
Loading
Loading