xtask: Handle macros crates in release

This commit is contained in:
Kévin Commaille 2021-04-12 19:21:58 +02:00 committed by Jonas Platte
parent b89a18fa16
commit 508aa6dac5
3 changed files with 124 additions and 93 deletions

View File

@ -16,6 +16,7 @@ use xshell::read_file;
mod ci; mod ci;
mod flags; mod flags;
mod release; mod release;
mod util;
use self::{ci::CiTask, release::ReleaseTask}; use self::{ci::CiTask, release::ReleaseTask};

View File

@ -1,7 +1,4 @@
use std::{ use std::path::{Path, PathBuf};
io::{stdin, stdout, BufRead, Write},
path::{Path, PathBuf},
};
use isahc::{ use isahc::{
auth::{Authentication, Credentials}, auth::{Authentication, Credentials},
@ -16,22 +13,20 @@ use serde_json::json;
use toml::from_str as from_toml_str; use toml::from_str as from_toml_str;
use xshell::{pushd, read_file}; use xshell::{pushd, read_file};
use crate::{cmd, config, Result}; use crate::{cmd, config, util::ask_yes_no, Result};
const CRATESIO_API: &str = "https://crates.io/api/v1/crates"; const CRATESIO_API: &str = "https://crates.io/api/v1/crates";
const GITHUB_API_RUMA: &str = "https://api.github.com/repos/ruma/ruma"; const GITHUB_API_RUMA: &str = "https://api.github.com/repos/ruma/ruma";
/// Task to create a new release of the given crate. /// Task to create a new release of the given crate.
#[derive(Debug)]
pub struct ReleaseTask { pub struct ReleaseTask {
/// The crate to release. /// The crate to release.
name: String, local_crate: LocalCrate,
/// The root of the workspace. /// The root of the workspace.
project_root: PathBuf, project_root: PathBuf,
/// The version to release.
version: Version,
/// The http client to use for requests. /// The http client to use for requests.
client: HttpClient, client: HttpClient,
} }
@ -39,61 +34,55 @@ pub struct ReleaseTask {
impl ReleaseTask { impl ReleaseTask {
/// Create a new `ReleaseTask` with the given `name` and `project_root`. /// Create a new `ReleaseTask` with the given `name` and `project_root`.
pub(crate) fn new(name: String, project_root: PathBuf) -> Result<Self> { pub(crate) fn new(name: String, project_root: PathBuf) -> Result<Self> {
let path = project_root.join(&name); let local_crate = LocalCrate::new(name, &project_root)?;
let version = Self::get_version(&path)?; Ok(Self { local_crate, project_root, client: HttpClient::new()? })
Ok(Self { name, project_root, version, client: HttpClient::new()? })
} }
/// Run the task to effectively create a release. /// Run the task to effectively create a release.
pub(crate) fn run(self) -> Result<()> { pub(crate) fn run(self) -> Result<()> {
println!("Starting release for {} {}", self.name, self.version); let title = &self.title();
println!("Starting release for {}", title);
if self.is_released()? { if self.is_released()? {
return Err("This version is already released".into()); return Err("This crate version is already released".into());
} }
let _dir = pushd(self.crate_path())?;
let remote = Self::git_remote()?; let remote = Self::git_remote()?;
println!("Checking status of git repository…"); println!("Checking status of git repository…");
if !cmd!("git status -s -uno").read()?.is_empty() if !cmd!("git status -s -uno").read()?.is_empty()
&& !Self::ask_continue("This git repository contains untracked files. Continue?")? && !ask_yes_no("This git repository contains untracked files. Continue?")?
{ {
return Ok(()); return Ok(());
} }
println!("Publishing the package on crates.io…"); if let Some(macros) = self.macros() {
if self.is_published()? print!("Found macros crate. ");
&& !Self::ask_continue( let _dir = pushd(&macros.path)?;
"This version is already published. Skip this step and continue?", macros.publish(&self.client)?;
)? println!("Resuming release of {}", self.title());
{
return Ok(());
} else {
cmd!("cargo publish").run()?;
} }
let changes = &self.get_changes()?; let _dir = pushd(&self.local_crate.path)?;
self.local_crate.publish(&self.client)?;
let changes = &self.local_crate.changes()?;
let tag = &self.tag_name(); let tag = &self.tag_name();
let title = &self.title();
println!("Creating git tag…"); println!("Creating git tag…");
if cmd!("git tag -l {tag}").read()?.is_empty() { if cmd!("git tag -l {tag}").read()?.is_empty() {
cmd!("git tag -s {tag} -m {title} -m {changes}").read()?; cmd!("git tag -s {tag} -m {title} -m {changes}").read()?;
} else if !Self::ask_continue("This tag already exists. Skip this step and continue?")? { } else if !ask_yes_no("This tag already exists. Skip this step and continue?")? {
return Ok(()); return Ok(());
} }
println!("Pushing tag to remote repository…"); println!("Pushing tag to remote repository…");
if cmd!("git ls-remote --tags {remote} {tag}").read()?.is_empty() { if cmd!("git ls-remote --tags {remote} {tag}").read()?.is_empty() {
cmd!("git push {remote} {tag}").run()?; cmd!("git push {remote} {tag}").run()?;
} else if !Self::ask_continue( } else if !ask_yes_no("This tag has already been pushed. Skip this step and continue?")? {
"This tag has already been pushed. Skip this step and continue?",
)? {
return Ok(()); return Ok(());
} }
@ -112,53 +101,14 @@ impl ReleaseTask {
Ok(()) Ok(())
} }
/// Ask the user if he wants to skip this step and continue. Returns `true` for yes. /// Get the associated `-macros` crate of the current crate, if any.
fn ask_continue(message: &str) -> Result<bool> { fn macros(&self) -> Option<LocalCrate> {
let mut input = String::new(); LocalCrate::new(format!("{}-macros", self.local_crate.name), &self.project_root).ok()
let stdin = stdin();
print!("{} [y/N]: ", message);
stdout().flush()?;
let mut handle = stdin.lock();
handle.read_line(&mut input)?;
input = input.trim().to_ascii_lowercase();
Ok(input == "y" || input == "yes")
}
/// Get the changes of the given version from the changelog.
fn get_changes(&self) -> Result<String> {
let changelog = read_file(self.crate_path().join("CHANGELOG.md"))?;
let lines_nb = changelog.lines().count();
let mut lines = changelog.lines();
let start = match lines.position(|l| l.starts_with(&format!("# {}", self.version))) {
Some(p) => p + 1,
None => {
return Err("Could not find version title in changelog".into());
}
};
let length = match lines.position(|l| l.starts_with("# ")) {
Some(p) => p,
None => lines_nb,
};
let changes = changelog.lines().skip(start).take(length).join("\n");
Ok(changes.trim().to_owned())
} }
/// Get the title of this release. /// Get the title of this release.
fn title(&self) -> String { fn title(&self) -> String {
format!("{} {}", self.name, self.version) format!("{} {}", self.local_crate.name, self.local_crate.version)
}
/// Get the path of the crate for this release.
fn crate_path(&self) -> PathBuf {
self.project_root.join(&self.name)
} }
/// Load the GitHub config from the config file. /// Load the GitHub config from the config file.
@ -175,23 +125,7 @@ impl ReleaseTask {
/// Get the tag name for this release. /// Get the tag name for this release.
fn tag_name(&self) -> String { fn tag_name(&self) -> String {
format!("{}-{}", self.name, self.version) format!("{}-{}", self.local_crate.name, self.local_crate.version)
}
/// Get the current version of the crate at `path` from its manifest.
fn get_version(path: &Path) -> Result<Version> {
let manifest_toml = read_file(path.join("Cargo.toml"))?;
let manifest: CargoManifest = from_toml_str(&manifest_toml)?;
Ok(manifest.package.version)
}
/// Check if the current version of the crate is published on crates.io.
fn is_published(&self) -> Result<bool> {
let response: CratesIoCrate =
self.client.get(format!("{}/{}/{}", CRATESIO_API, self.name, self.version))?.json()?;
Ok(response.version.is_some())
} }
/// Check if the tag for the current version of the crate has been pushed on GitHub. /// Check if the tag for the current version of the crate has been pushed on GitHub.
@ -222,6 +156,83 @@ impl ReleaseTask {
} }
} }
/// A local Rust crate.
#[derive(Debug)]
struct LocalCrate {
/// The name of the crate.
name: String,
/// The version of the crate.
version: Version,
/// The local path of the crate.
path: PathBuf,
}
impl LocalCrate {
/// Creates a new `Crate` with the given name and project root.
pub fn new(name: String, project_root: &PathBuf) -> Result<Self> {
let path = project_root.join(&name);
let version = Self::version(&path)?;
Ok(Self { name, version, path })
}
/// The current version of the crate at `path` from its manifest.
fn version(path: &Path) -> Result<Version> {
let manifest_toml = read_file(path.join("Cargo.toml"))?;
let manifest: CargoManifest = from_toml_str(&manifest_toml)?;
Ok(manifest.package.version)
}
/// The changes of the given version from the changelog.
fn changes(&self) -> Result<String> {
let changelog = read_file(self.path.join("CHANGELOG.md"))?;
let lines_nb = changelog.lines().count();
let mut lines = changelog.lines();
let start = match lines.position(|l| l.starts_with(&format!("# {}", self.version))) {
Some(p) => p + 1,
None => {
return Err("Could not find version title in changelog".into());
}
};
let length = match lines.position(|l| l.starts_with("# ")) {
Some(p) => p,
None => lines_nb,
};
let changes = changelog.lines().skip(start).take(length).join("\n");
Ok(changes.trim().to_owned())
}
/// Check if the current version of the crate is published on crates.io.
fn is_published(&self, client: &HttpClient) -> Result<bool> {
let response: CratesIoCrate =
client.get(format!("{}/{}/{}", CRATESIO_API, self.name, self.version))?.json()?;
Ok(response.version.is_some())
}
/// Publish this package on crates.io.
fn publish(&self, client: &HttpClient) -> Result<()> {
println!("Publishing {} {} on crates.io…", self.name, self.version);
if self.is_published(client)? {
if ask_yes_no("This version is already published. Skip this step and continue?")? {
Ok(())
} else {
Err("Release interrupted by user.")?
}
} else {
Ok(cmd!("cargo publish").run()?)
}
}
}
/// The required cargo manifest data of a crate. /// The required cargo manifest data of a crate.
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct CargoManifest { struct CargoManifest {

19
xtask/src/util.rs Normal file
View File

@ -0,0 +1,19 @@
use std::io::{stdin, stdout, BufRead, Write};
use crate::Result;
/// Ask the user the given yes or no question and wait for their input. Returns `true` for yes.
pub fn ask_yes_no(question: &str) -> Result<bool> {
let mut input = String::new();
let stdin = stdin();
print!("{} [y/N]: ", question);
stdout().flush()?;
let mut handle = stdin.lock();
handle.read_line(&mut input)?;
input = input.trim().to_ascii_lowercase();
Ok(input == "y" || input == "yes")
}