Add subcommand to rollover migration guides (#3606)

* add subcommand to create migration guides

* make git changes
This commit is contained in:
Scott Mabin 2025-06-09 15:18:53 +01:00 committed by GitHub
parent 1f1e120dd2
commit 71fe3f0e46
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 141 additions and 11 deletions

View File

@ -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:

View File

@ -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)) {

View File

@ -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<Branch> {
// 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<String> {
pub(crate) fn comparison_url(base: &str, url: &str, branch_name: &str) -> Result<String> {
let url = if url.starts_with("https://github.com/esp-rs/") {
format!("https://github.com/esp-rs/esp-hal/compare/{base}...{branch_name}")
} else {

View File

@ -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::<toml_edit::DocumentMut>()?;
// 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(())
}

View File

@ -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::*;

View File

@ -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),