diff --git a/xtask/src/commands/release.rs b/xtask/src/commands/release.rs index 8b03d29e7..72e8d8afa 100644 --- a/xtask/src/commands/release.rs +++ b/xtask/src/commands/release.rs @@ -5,6 +5,8 @@ pub mod bump_version; pub mod execute_plan; #[cfg(feature = "release")] pub mod plan; +#[cfg(feature = "release")] +pub mod post_release; pub mod publish; #[cfg(feature = "release")] pub mod publish_plan; @@ -21,6 +23,10 @@ pub use publish::*; pub use publish_plan::*; pub use semver_check::*; pub use tag_releases::*; +#[cfg(feature = "release")] +pub use post_release::*; + +pub const PLACEHOLDER: &str = "{{currentVersion}}"; // ---------------------------------------------------------------------------- // Subcommands @@ -47,6 +53,10 @@ pub enum Release { /// the release and pushes the tags. #[cfg(feature = "release")] PublishPlan(PublishPlanArgs), + /// Rollover migrations steps post release. + /// - Create new migration guides for packages that have a migration guide + #[cfg(feature = "release")] + PostRelease, /// Bump the version of the specified package(s). /// /// This command will, for each specified package: diff --git a/xtask/src/commands/release/bump_version.rs b/xtask/src/commands/release/bump_version.rs index 486199fe3..9904df2f4 100644 --- a/xtask/src/commands/release/bump_version.rs +++ b/xtask/src/commands/release/bump_version.rs @@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize}; use strum::IntoEnumIterator; use toml_edit::{Item, TableLike, Value}; -use crate::{Package, Version, cargo::CargoToml, changelog::Changelog}; +use crate::{cargo::CargoToml, changelog::Changelog, commands::PLACEHOLDER, Package, Version}; #[derive(Debug, Clone, Deserialize, Serialize)] pub enum VersionBump { @@ -308,8 +308,6 @@ fn finalize_placeholders( new_version: &semver::Version, dry_run: bool, ) -> Result<()> { - const PLACEHOLDER: &str = "{{currentVersion}}"; - let skip_paths = [bumped_package.package_path().join("target")]; fn walk_dir(dir: &Path, skip_paths: &[PathBuf], callback: &mut impl FnMut(&Path)) { diff --git a/xtask/src/commands/release/execute_plan.rs b/xtask/src/commands/release/execute_plan.rs index 90a8ecc68..0ba39e7b9 100644 --- a/xtask/src/commands/release/execute_plan.rs +++ b/xtask/src/commands/release/execute_plan.rs @@ -98,7 +98,10 @@ pub fn execute_plan(workspace: &Path, args: ApplyPlanArgs) -> Result<()> { ); } - make_git_changes(!args.no_dry_run, &plan_source, &plan)?; + let branch = make_git_changes(!args.no_dry_run, "release-branch", "Finalize crate releases")?; + + open_pull_request(&branch, !args.no_dry_run, &plan_source, &plan) + .with_context(|| "Failed to open pull request")?; if !args.no_dry_run { println!( @@ -109,12 +112,16 @@ pub fn execute_plan(workspace: &Path, args: ApplyPlanArgs) -> Result<()> { Ok(()) } -fn make_git_changes(dry_run: bool, release_plan_str: &str, release_plan: &Plan) -> Result<()> { +pub(crate) struct Branch { + pub name: String, + pub upstream: String, +} + +pub(crate) fn make_git_changes(dry_run: bool, branch_name: &str, commit: &str) -> Result { // Find an available branch name let branch_name = format!( "{branch_name}-{}", jiff::Timestamp::now().strftime("%Y-%m-%d"), - branch_name = "release-branch", ); let upstream = get_remote_name_for("esp-rs/esp-hal")?; @@ -140,10 +147,11 @@ fn make_git_changes(dry_run: bool, release_plan_str: &str, release_plan: &Plan) if dry_run { println!("Dry run: would commit changes to branch: {branch_name}"); } else { + Command::new("git").arg("add").arg(".").status().context("Failed to stage changes")?; Command::new("git") .arg("commit") - .arg("-am") - .arg("Finalize crates for release") + .arg("-m") + .arg(commit) .status() .context("Failed to commit changes")?; } @@ -174,6 +182,18 @@ fn make_git_changes(dry_run: bool, release_plan_str: &str, release_plan: &Plan) extract_url_from_push(&String::from_utf8_lossy(&message.stderr)) // git outputs to stderr }; + Ok(Branch { + name: branch_name, + upstream: url, + }) +} + +fn open_pull_request( + branch: &Branch, + dry_run: bool, + release_plan_str: &str, + release_plan: &Plan, +) -> Result<()> { // Open a pull request let packages_to_release = release_plan @@ -216,7 +236,7 @@ cargo xrelease publish-plan --no-dry-run // TODO: don't forget to update the PR text once we have the `publish` command // updated. - let pr_url_base = comparison_url(&release_plan.base, &url, &branch_name)?; + let pr_url_base = comparison_url(&release_plan.base, &branch.upstream, &branch.name)?; // Query string options are documented at: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/using-query-parameters-to-create-a-pull-request let mut open_pr_url = format!( @@ -254,7 +274,6 @@ cargo xrelease publish-plan --no-dry-run } println!("Create the release PR and follow the next steps laid out there."); - Ok(()) } @@ -268,7 +287,7 @@ fn extract_url_from_push(output: &str) -> String { .to_string() } -fn comparison_url(base: &str, url: &str, branch_name: &str) -> Result { +pub(crate) fn comparison_url(base: &str, url: &str, branch_name: &str) -> Result { let url = if url.starts_with("https://github.com/esp-rs/") { format!("https://github.com/esp-rs/esp-hal/compare/{base}...{branch_name}") } else { diff --git a/xtask/src/commands/release/post_release.rs b/xtask/src/commands/release/post_release.rs new file mode 100644 index 000000000..76196db27 --- /dev/null +++ b/xtask/src/commands/release/post_release.rs @@ -0,0 +1,81 @@ +use std::fs; + +use anyhow::{Context, Result}; +use semver::Version; +use super::execute_plan::make_git_changes; +use super::PLACEHOLDER; +use super::Plan; +use crate::commands::comparison_url; + +pub fn post_release(workspace: &std::path::Path) -> Result<()> { + // Read the release plan + let plan_path = workspace.join("release_plan.jsonc"); + let plan_path = crate::windows_safe_path(&plan_path); + + let plan = Plan::from_path(&plan_path) + .with_context(|| format!("Failed to read release plan from {}", plan_path.display()))?; + + // Process packages from the plan that have migration guides + for package_plan in plan.packages.iter() { + let package = package_plan.package; + + if !package.has_migration_guide(workspace) { + continue; + } + + // Get the package's directory path + let package_path = workspace.join(package.to_string()); + let cargo_toml_path = package_path.join("Cargo.toml"); + + // Read and parse Cargo.toml + let cargo_toml_content = fs::read_to_string(&cargo_toml_path)?; + let cargo_toml = cargo_toml_content.parse::()?; + + // Extract version from Cargo.toml + let version_str = cargo_toml["package"]["version"].as_str().ok_or_else(|| { + anyhow::anyhow!( + "Could not find version in Cargo.toml for package {:?}", + package + ) + })?; + + // Parse version using semver and zero out patch version + let mut version = Version::parse(version_str)?; + version.patch = 0; + + // Generate migration guide filename + let migration_file_name = format!("MIGRATING-{}.md", version); + let migration_file_path = package_path.join(&migration_file_name); + + // Create the migration guide file if it doesn't exist + if !migration_file_path.exists() { + // Create the title content + let title = format!("# Migration Guide from {} to {}\n", version, PLACEHOLDER); + fs::write(&migration_file_path, title)?; + log::info!("Created migration guide: {}", migration_file_path.display()); + } else { + log::info!( + "Migration guide already exists: {}", + migration_file_path.display() + ); + } + } + + let branch = make_git_changes(false, "post-release-branch", "Post release rollover")?; + + println!("Post-release migration guides created successfully."); + + let pr_url_base = comparison_url(&plan.base, &branch.upstream, &branch.name)?; + + let open_pr_url = format!( + "{pr_url_base}?quick_pull=1&title=Post+release+rollover&labels={labels}", + labels = "skip-changelog", + ); + + if opener::open(&open_pr_url).is_err() { + println!("Open the following URL to create a pull request:"); + println!("{open_pr_url}"); + } + + Ok(()) +} diff --git a/xtask/src/lib.rs b/xtask/src/lib.rs index ac9ae3d6f..b33089286 100644 --- a/xtask/src/lib.rs +++ b/xtask/src/lib.rs @@ -106,6 +106,26 @@ impl Package { .any(|line| line.contains("asm_experimental_arch")) } + pub fn has_migration_guide(&self, workspace: &Path) -> bool { + let package_path = workspace.join(self.to_string()); + + // Check if the package directory exists + let Ok(entries) = std::fs::read_dir(&package_path) else { + return false; + }; + + // Look for files matching the pattern "MIGRATING-*.md" + for entry in entries.flatten() { + if let Some(file_name) = entry.file_name().to_str() { + if file_name.starts_with("MIGRATING-") && file_name.ends_with(".md") { + return true; + } + } + } + + false + } + pub fn needs_build_std(&self) -> bool { use Package::*; diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 0fd2988c2..fbcbb61aa 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -133,6 +133,8 @@ fn main() -> Result<()> { Release::ExecutePlan(args) => execute_plan(&workspace, args), #[cfg(feature = "release")] Release::PublishPlan(args) => publish_plan(&workspace, args), + #[cfg(feature = "release")] + Release::PostRelease => post_release(&workspace), }, Cli::Ci(args) => run_ci_checks(&workspace, args),