mirror of
https://github.com/esp-rs/esp-hal.git
synced 2025-09-29 21:30:39 +00:00
Publish based on release plan (#3542)
* Publish and tag based on release plan * Update wording * Update xtask/src/commands/release/execute_plan.rs Co-authored-by: Juraj Sadel <jurajsadel@gmail.com> --------- Co-authored-by: Juraj Sadel <jurajsadel@gmail.com>
This commit is contained in:
parent
0f4b29d49d
commit
692b7dddc2
@ -6,6 +6,8 @@ pub mod execute_plan;
|
||||
#[cfg(feature = "release")]
|
||||
pub mod plan;
|
||||
pub mod publish;
|
||||
#[cfg(feature = "release")]
|
||||
pub mod publish_plan;
|
||||
pub mod semver_check;
|
||||
pub mod tag_releases;
|
||||
|
||||
@ -15,6 +17,8 @@ pub use execute_plan::*;
|
||||
#[cfg(feature = "release")]
|
||||
pub use plan::*;
|
||||
pub use publish::*;
|
||||
#[cfg(feature = "release")]
|
||||
pub use publish_plan::*;
|
||||
pub use semver_check::*;
|
||||
pub use tag_releases::*;
|
||||
|
||||
@ -35,6 +39,14 @@ pub enum Release {
|
||||
/// and opens a pull request with the changes.
|
||||
#[cfg(feature = "release")]
|
||||
ExecutePlan(ApplyPlanArgs),
|
||||
/// Attempt to publish the specified package.
|
||||
///
|
||||
/// This command will double check based on the release plan that the
|
||||
/// command is called on the right branch, and that the crate(s) to be
|
||||
/// released have the right version. After publishing, the command tags
|
||||
/// the release and pushes the tags.
|
||||
#[cfg(feature = "release")]
|
||||
PublishPlan(PublishPlanArgs),
|
||||
/// Bump the version of the specified package(s).
|
||||
///
|
||||
/// This command will, for each specified package:
|
||||
|
@ -6,7 +6,7 @@ use clap::Args;
|
||||
use crate::{
|
||||
cargo::CargoToml,
|
||||
commands::{release::plan::Plan, update_package},
|
||||
git::current_branch,
|
||||
git::{current_branch, ensure_workspace_clean, get_remote_name_for},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
@ -23,17 +23,8 @@ pub fn execute_plan(workspace: &Path, args: ApplyPlanArgs) -> Result<()> {
|
||||
let plan_path = workspace.join("release_plan.jsonc");
|
||||
let plan_path = crate::windows_safe_path(&plan_path);
|
||||
|
||||
let plan_source = std::fs::read_to_string(&plan_path)
|
||||
.with_context(|| format!("Failed to read release plan from {}. Run `cargo xrelease plan` to generate a release plan.", plan_path.display()))?;
|
||||
|
||||
if plan_source.lines().any(|line| line.starts_with("//")) {
|
||||
bail!(
|
||||
"The release plan has not been finalized. Please open the plan and follow the instructions in it."
|
||||
);
|
||||
}
|
||||
|
||||
let mut plan = serde_json::from_str::<Plan>(&plan_source)
|
||||
.with_context(|| format!("Failed to parse release plan from {}", plan_path.display()))?;
|
||||
let mut plan = Plan::from_path(&plan_path)
|
||||
.with_context(|| format!("Failed to read release plan from {}", plan_path.display()))?;
|
||||
|
||||
ensure!(
|
||||
current_branch()? == plan.base,
|
||||
@ -95,24 +86,6 @@ pub fn execute_plan(workspace: &Path, args: ApplyPlanArgs) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_workspace_clean(workspace: &Path) -> Result<()> {
|
||||
std::env::set_current_dir(workspace)
|
||||
.with_context(|| format!("Failed to change directory to {}", workspace.display()))?;
|
||||
|
||||
let status = Command::new("git")
|
||||
.arg("status")
|
||||
.arg("--porcelain")
|
||||
.output()
|
||||
.context("Failed to check git status")?;
|
||||
|
||||
ensure!(
|
||||
String::from_utf8_lossy(&status.stdout).trim().is_empty(),
|
||||
"The workspace is not clean. Please commit or stash your changes before running this command."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn make_git_changes(dry_run: bool, release_plan_str: &str, release_plan: &Plan) -> Result<()> {
|
||||
// Find an available branch name
|
||||
let branch_name = format!(
|
||||
@ -198,9 +171,16 @@ The release plan used for this release:
|
||||
{release_plan_str}
|
||||
```
|
||||
|
||||
Please review the changes and merge them into the main branch.
|
||||
Once merged, the packages will be ready to be published and tagged.
|
||||
"#
|
||||
Please review the changes and merge them into the `{base_branch}` branch.
|
||||
|
||||
After merging, please make sure you have this release plan in the repo root,
|
||||
then run the following command on the `{base_branch}` branch to tag and publish the packages:
|
||||
|
||||
```
|
||||
cargo xrelease publish-plan --no-dry-run
|
||||
```
|
||||
"#,
|
||||
base_branch = release_plan.base,
|
||||
);
|
||||
|
||||
if release_plan.base != "main" {
|
||||
@ -230,36 +210,11 @@ Once merged, the packages will be ready to be published and tagged.
|
||||
println!("{open_pr_url}");
|
||||
}
|
||||
|
||||
println!("Once you create and merge the pull request, check out current main.");
|
||||
println!("Make sure you have the release_plan.jsonc file in the root of the workspace.");
|
||||
// TODO: uncomment this once we have the `publish` command updated
|
||||
// println!("Next, run `cargo xrelease publish` to tag the release and publish
|
||||
// the packages.");
|
||||
println!("Create the release PR and follow the next steps laid out there.");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_remote_name_for(repo: &str) -> Result<String> {
|
||||
let remotes = Command::new("git")
|
||||
.arg("remote")
|
||||
.arg("-v")
|
||||
.output()
|
||||
.context("Failed to get remote URL")?;
|
||||
|
||||
let remotes = String::from_utf8_lossy(&remotes.stdout);
|
||||
|
||||
for line in remotes.lines() {
|
||||
if line.contains(repo) {
|
||||
let parts: Vec<_> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
return Ok(parts[0].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bail!("Failed to find remote name for {repo}");
|
||||
}
|
||||
|
||||
fn extract_url_from_push(output: &str) -> String {
|
||||
output
|
||||
.lines()
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{collections::HashMap, io::Write, path::Path, process::Command};
|
||||
|
||||
use anyhow::{Result, ensure};
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
use cargo_semver_checks::ReleaseType;
|
||||
use clap::Args;
|
||||
use esp_metadata::Chip;
|
||||
@ -48,6 +48,25 @@ pub struct Plan {
|
||||
pub packages: Vec<PackagePlan>,
|
||||
}
|
||||
|
||||
impl Plan {
|
||||
pub fn from_path(plan_path: &Path) -> Result<Self> {
|
||||
let plan_source = std::fs::read_to_string(&plan_path)
|
||||
.with_context(|| format!("Failed to read release plan from {}. Run `cargo xrelease plan` to generate a release plan.", plan_path.display()))?;
|
||||
|
||||
if plan_source.lines().any(|line| line.starts_with("//")) {
|
||||
bail!(
|
||||
"The release plan has not been finalized. Please open the plan and follow the instructions in it."
|
||||
);
|
||||
}
|
||||
|
||||
let plan = serde_json::from_str::<Plan>(&plan_source).with_context(|| {
|
||||
format!("Failed to parse release plan from {}", plan_path.display())
|
||||
})?;
|
||||
|
||||
Ok(plan)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn plan(workspace: &Path, args: PlanArgs) -> Result<()> {
|
||||
let current_branch = ensure_main_branch(args.allow_non_main)?;
|
||||
|
||||
|
146
xtask/src/commands/release/publish_plan.rs
Normal file
146
xtask/src/commands/release/publish_plan.rs
Normal file
@ -0,0 +1,146 @@
|
||||
use std::{path::Path, process::Command};
|
||||
|
||||
use anyhow::{Context, Result, bail, ensure};
|
||||
use clap::Args;
|
||||
|
||||
use crate::{
|
||||
cargo::{CargoArgsBuilder, CargoToml},
|
||||
commands::Plan,
|
||||
git::{current_branch, ensure_workspace_clean, get_remote_name_for},
|
||||
};
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
pub struct PublishPlanArgs {
|
||||
/// Do not pass the `--dry-run` argument, actually try to publish.
|
||||
#[arg(long)]
|
||||
no_dry_run: bool,
|
||||
}
|
||||
|
||||
pub fn publish_plan(workspace: &Path, args: PublishPlanArgs) -> Result<()> {
|
||||
ensure_workspace_clean(workspace)?;
|
||||
|
||||
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()))?;
|
||||
|
||||
// Check that the publish command is being run on the right branch. This is
|
||||
// meant to prevent publishing from the release branch before the changes
|
||||
// are reviewed and merged.
|
||||
ensure!(
|
||||
current_branch()? == plan.base,
|
||||
"The packages must be published from the same branch the plan was created on. \
|
||||
Please switch to the {} branch and try again.",
|
||||
plan.base
|
||||
);
|
||||
|
||||
let tomls = plan
|
||||
.packages
|
||||
.iter()
|
||||
.map(|step| {
|
||||
CargoToml::new(workspace, step.package)
|
||||
.with_context(|| format!("Failed to read Cargo.toml for {}", step.package))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
// 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()) {
|
||||
if toml.package_version() != step.new_version {
|
||||
if toml.package_version() == step.current_version {
|
||||
bail!(
|
||||
"Package {} has not been updated yet. Run `cargo xrelease execute-plan` before publishing the packages.",
|
||||
step.package
|
||||
);
|
||||
}
|
||||
bail!(
|
||||
"The version of package {} has changed in an unexpected way. Cannot continue.",
|
||||
step.package
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Actually publish the packages.
|
||||
for (step, toml) in plan.packages.iter().zip(tomls.iter()) {
|
||||
let mut publish_args = if step.package.has_chip_features() {
|
||||
vec!["--no-verify"]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
if !args.no_dry_run {
|
||||
publish_args.push("--dry-run");
|
||||
}
|
||||
|
||||
let builder = CargoArgsBuilder::default()
|
||||
.subcommand("publish")
|
||||
.args(&publish_args);
|
||||
|
||||
let args = builder.build();
|
||||
log::debug!("{args:#?}");
|
||||
|
||||
// Execute `cargo publish` command from the package root:
|
||||
crate::cargo::run(&args, &toml.package_path())?;
|
||||
}
|
||||
|
||||
// Tag the releases
|
||||
let mut new_tags = Vec::new();
|
||||
for (step, toml) in plan.packages.iter().zip(tomls.iter()) {
|
||||
let tag_name = toml.package.tag(&toml.package_version());
|
||||
let tag_message = format!("{} {}", step.package, toml.version());
|
||||
|
||||
if args.no_dry_run {
|
||||
let output = Command::new("git")
|
||||
.arg("tag")
|
||||
.arg("-m")
|
||||
.arg(&tag_message)
|
||||
.arg(&tag_name)
|
||||
.current_dir(workspace)
|
||||
.output()
|
||||
.context("Failed to create git tag")?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"Failed to create git tag {}: {}",
|
||||
tag_name,
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
println!("Tagged {} with message: {}", tag_name, tag_message);
|
||||
} else {
|
||||
println!(
|
||||
"Dry run: would tag {} with message: {}",
|
||||
tag_name, tag_message
|
||||
);
|
||||
}
|
||||
|
||||
new_tags.push(tag_name);
|
||||
}
|
||||
|
||||
let upstream = get_remote_name_for("esp-rs/esp-hal")?;
|
||||
if args.no_dry_run {
|
||||
let output = Command::new("git")
|
||||
.arg("push")
|
||||
.arg(upstream)
|
||||
.args(&new_tags)
|
||||
.output()
|
||||
.context("Failed to push git tags")?;
|
||||
|
||||
if !output.status.success() {
|
||||
bail!(
|
||||
"Failed to push git tags: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"Dry run: would push the following tags to {upstream}: {}",
|
||||
new_tags.join(", ")
|
||||
);
|
||||
}
|
||||
|
||||
println!("publish-plan completed successfully.");
|
||||
|
||||
Ok(())
|
||||
}
|
@ -12,3 +12,44 @@ pub fn current_branch() -> Result<String> {
|
||||
|
||||
Ok(String::from_utf8_lossy(&status.stdout).trim().to_string())
|
||||
}
|
||||
|
||||
#[cfg(feature = "release")]
|
||||
pub fn ensure_workspace_clean(workspace: &std::path::Path) -> Result<()> {
|
||||
std::env::set_current_dir(workspace)
|
||||
.with_context(|| format!("Failed to change directory to {}", workspace.display()))?;
|
||||
|
||||
let status = Command::new("git")
|
||||
.arg("status")
|
||||
.arg("--porcelain")
|
||||
.output()
|
||||
.context("Failed to check git status")?;
|
||||
|
||||
anyhow::ensure!(
|
||||
String::from_utf8_lossy(&status.stdout).trim().is_empty(),
|
||||
"The workspace is not clean. Please commit or stash your changes before running this command."
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "release")]
|
||||
pub fn get_remote_name_for(repo: &str) -> Result<String> {
|
||||
let remotes = Command::new("git")
|
||||
.arg("remote")
|
||||
.arg("-v")
|
||||
.output()
|
||||
.context("Failed to get remote URL")?;
|
||||
|
||||
let remotes = String::from_utf8_lossy(&remotes.stdout);
|
||||
|
||||
for line in remotes.lines() {
|
||||
if line.contains(repo) {
|
||||
let parts: Vec<_> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
return Ok(parts[0].to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::bail!("Failed to find remote name for {repo}");
|
||||
}
|
||||
|
@ -131,6 +131,8 @@ fn main() -> Result<()> {
|
||||
Release::Plan(args) => plan(&workspace, args),
|
||||
#[cfg(feature = "release")]
|
||||
Release::ExecutePlan(args) => execute_plan(&workspace, args),
|
||||
#[cfg(feature = "release")]
|
||||
Release::PublishPlan(args) => publish_plan(&workspace, args),
|
||||
},
|
||||
|
||||
Cli::Ci(args) => run_ci_checks(&workspace, args),
|
||||
|
Loading…
x
Reference in New Issue
Block a user