xtask: Handle macros crates in release
This commit is contained in:
parent
b89a18fa16
commit
508aa6dac5
@ -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};
|
||||||
|
|
||||||
|
@ -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(¯os.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
19
xtask/src/util.rs
Normal 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")
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user