mirror of
https://github.com/girlbossceo/ruwuma.git
synced 2025-04-29 06:49:48 -04:00
xtask: Add release commit creation to release
This commit is contained in:
parent
3c237652db
commit
bc62192e60
7 changed files with 469 additions and 173 deletions
|
@ -9,10 +9,10 @@ publish = false
|
|||
|
||||
[dependencies]
|
||||
isahc = { version = "1.2.0", features = ["json"] }
|
||||
itertools = "0.10.0"
|
||||
semver = { version = "0.11.0", features = ["serde"] }
|
||||
serde = { version = "1.0.118", features = ["derive"] }
|
||||
serde_json = "1.0.60"
|
||||
toml = "0.5.8"
|
||||
toml_edit = "0.2.0"
|
||||
xflags = "0.2.1"
|
||||
xshell = "0.1.9"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Ruma xtasks
|
||||
|
||||
This crate is a helper bin for repetitive tasks during Ruma development, based on [cargo-xtask].
|
||||
This crate is a helper bin for repetitive tasks during Ruma development, based on [cargo xtask][xtask].
|
||||
|
||||
To use it, run `cargo xtask [command]` anywhere in the workspace.
|
||||
|
||||
|
@ -9,7 +9,10 @@ the appropriate fields.
|
|||
|
||||
## Commands
|
||||
|
||||
- `release [crate]`: Publish `crate`, create a signed tag based on its name and version and create
|
||||
a release on GitHub. **Requires all `github` fields in `config.toml`.**
|
||||
- `release [crate] [version]`: Publish `crate` at given `version`, if applicable<sup>[1](#ref-1)</sup>, create a
|
||||
signed tag based on its name and version and create a release on GitHub.
|
||||
**Requires all `github` fields in `config.toml`.**
|
||||
|
||||
[cargo-xtask] : https://github.com/matklad/cargo-xtask
|
||||
<sup><span id="ref-1">1</span></sup> if `crate` is a user-facing crate and `version` is not a pre-release.
|
||||
|
||||
[xtask]: https://github.com/matklad/cargo-xtask
|
||||
|
|
192
xtask/src/cargo.rs
Normal file
192
xtask/src/cargo.rs
Normal file
|
@ -0,0 +1,192 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use isahc::{HttpClient, ReadResponseExt};
|
||||
use semver::Version;
|
||||
use serde::{de::IgnoredAny, Deserialize};
|
||||
use serde_json::from_str as from_json_str;
|
||||
use toml_edit::{value, Document};
|
||||
use xshell::{cmd, pushd, read_file, write_file};
|
||||
|
||||
use crate::{util::ask_yes_no, Result};
|
||||
|
||||
const CRATESIO_API: &str = "https://crates.io/api/v1/crates";
|
||||
|
||||
/// The metadata of a cargo workspace.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Metadata {
|
||||
pub workspace_root: PathBuf,
|
||||
pub packages: Vec<Package>,
|
||||
}
|
||||
|
||||
impl Metadata {
|
||||
/// Load a new `Metadata` from the command line.
|
||||
pub fn load() -> Result<Metadata> {
|
||||
let metadata_json = cmd!("cargo metadata --no-deps --format-version 1").read()?;
|
||||
Ok(from_json_str(&metadata_json)?)
|
||||
}
|
||||
}
|
||||
|
||||
/// A cargo package.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Package {
|
||||
/// The package name
|
||||
pub name: String,
|
||||
|
||||
/// The package version.
|
||||
pub version: Version,
|
||||
|
||||
/// The package's manifest path.
|
||||
pub manifest_path: PathBuf,
|
||||
|
||||
/// A map of the package dependencies.
|
||||
#[serde(default)]
|
||||
pub dependencies: Vec<Dependency>,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
/// Update the version of this crate.
|
||||
pub fn update_version(&mut self, version: &Version) -> Result<()> {
|
||||
println!("Updating {} to version {}…", self.name, version);
|
||||
|
||||
let mut document = read_file(&self.manifest_path)?.parse::<Document>()?;
|
||||
|
||||
document["package"]["version"] = value(version.to_string());
|
||||
|
||||
write_file(&self.manifest_path, document.to_string())?;
|
||||
|
||||
self.version = version.clone();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update the version of this crate in dependant crates' manifests, with the given version
|
||||
/// prefix.
|
||||
pub fn update_dependants(&self, metadata: &Metadata) -> Result<()> {
|
||||
for package in metadata.packages.iter().filter(|p| {
|
||||
p.manifest_path.starts_with(&metadata.workspace_root)
|
||||
&& p.dependencies.iter().any(|d| d.name == self.name)
|
||||
}) {
|
||||
println!("Updating dependency in {} crate…", package.name);
|
||||
|
||||
let mut document = read_file(&package.manifest_path)?.parse::<Document>()?;
|
||||
|
||||
for dependency in package.dependencies.iter().filter(|d| d.name == self.name) {
|
||||
let version = if self.version.is_prerelease() || self.name.ends_with("-macros") {
|
||||
format!("={}", self.version)
|
||||
} else {
|
||||
self.version.to_string()
|
||||
};
|
||||
|
||||
let kind = match dependency.kind {
|
||||
Some(DependencyKind::Dev) => "dev-dependencies",
|
||||
Some(DependencyKind::Build) => "build-dependencies",
|
||||
None => "dependencies",
|
||||
};
|
||||
|
||||
document[kind][&self.name]["version"] = value(version.as_str());
|
||||
}
|
||||
|
||||
write_file(&package.manifest_path, document.to_string())?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the changes for the version. If `update` is `true`, update the changelog for the release
|
||||
/// of the given version.
|
||||
pub fn changes(&self, update: bool) -> Result<String> {
|
||||
let mut changelog_path = self.manifest_path.clone();
|
||||
changelog_path.set_file_name("CHANGELOG.md");
|
||||
|
||||
let changelog = read_file(&changelog_path)?;
|
||||
|
||||
if !changelog.starts_with(&format!("# {}\n", self.version))
|
||||
&& !changelog.starts_with(&format!("# {} (unreleased)\n", self.version))
|
||||
&& !changelog.starts_with("# [unreleased]\n")
|
||||
{
|
||||
return Err("Could not find version title in changelog".into());
|
||||
};
|
||||
|
||||
let changes_start = match changelog.find('\n') {
|
||||
Some(p) => p + 1,
|
||||
None => {
|
||||
return Err("Could not find end of version title in changelog".into());
|
||||
}
|
||||
};
|
||||
|
||||
let changes_end = match changelog[changes_start..].find("\n# ") {
|
||||
Some(p) => changes_start + p,
|
||||
None => changelog.len(),
|
||||
};
|
||||
|
||||
let changes = match changelog[changes_start..changes_end].trim() {
|
||||
s if s.is_empty() => "No changes for this version",
|
||||
s => s,
|
||||
};
|
||||
|
||||
if update {
|
||||
let changelog = format!(
|
||||
"# [unreleased]\n\n# {}\n\n{}\n{}",
|
||||
self.version,
|
||||
changes,
|
||||
&changelog[changes_end..]
|
||||
);
|
||||
|
||||
write_file(&changelog_path, changelog)?;
|
||||
}
|
||||
|
||||
Ok(changes.to_owned())
|
||||
}
|
||||
|
||||
/// Check if the current version of the crate is published on crates.io.
|
||||
pub 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.
|
||||
pub fn publish(&self, client: &HttpClient) -> Result<bool> {
|
||||
println!("Publishing {} {} on crates.io…", self.name, self.version);
|
||||
let _dir = pushd(&self.manifest_path.parent().unwrap())?;
|
||||
|
||||
if self.is_published(client)? {
|
||||
if ask_yes_no("This version is already published. Skip this step and continue?")? {
|
||||
Ok(false)
|
||||
} else {
|
||||
Err("Release interrupted by user.".into())
|
||||
}
|
||||
} else {
|
||||
cmd!("cargo publish").run()?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A cargo package dependency.
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct Dependency {
|
||||
/// The package name.
|
||||
pub name: String,
|
||||
|
||||
/// The kind of the dependency.
|
||||
pub kind: Option<DependencyKind>,
|
||||
}
|
||||
|
||||
/// The kind of a cargo package dependency.
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DependencyKind {
|
||||
/// A dev dependency.
|
||||
Dev,
|
||||
|
||||
/// A build dependency.
|
||||
Build,
|
||||
}
|
||||
|
||||
/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
|
||||
#[derive(Deserialize)]
|
||||
struct CratesIoCrate {
|
||||
version: Option<IgnoredAny>,
|
||||
}
|
|
@ -4,7 +4,7 @@ use std::path::PathBuf;
|
|||
|
||||
use xshell::pushd;
|
||||
|
||||
use crate::{cmd, Result};
|
||||
use crate::{cargo::Metadata, cmd, Result};
|
||||
|
||||
const MSRV: &str = "1.45";
|
||||
|
||||
|
@ -18,8 +18,9 @@ pub struct CiTask {
|
|||
}
|
||||
|
||||
impl CiTask {
|
||||
pub(crate) fn new(version: Option<String>, project_root: PathBuf) -> Self {
|
||||
Self { version, project_root }
|
||||
pub(crate) fn new(version: Option<String>) -> Result<Self> {
|
||||
let project_root = Metadata::load()?.workspace_root;
|
||||
Ok(Self { version, project_root })
|
||||
}
|
||||
|
||||
pub(crate) fn run(self) -> Result<()> {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
#![allow(dead_code)] // silence never-used warning for from_vec in generated code
|
||||
|
||||
use semver::Version;
|
||||
|
||||
xflags::xflags! {
|
||||
src "./src/flags.rs"
|
||||
|
||||
|
@ -14,6 +16,18 @@ xflags::xflags! {
|
|||
cmd release
|
||||
/// The crate to release
|
||||
required name: String
|
||||
|
||||
/// The new version of the crate
|
||||
required version: Version
|
||||
{}
|
||||
|
||||
/// Alias for release.
|
||||
cmd publish
|
||||
/// The crate to release
|
||||
required name: String
|
||||
|
||||
/// The new version of the crate
|
||||
required version: Version
|
||||
{}
|
||||
|
||||
/// Run CI tests.
|
||||
|
@ -34,6 +48,7 @@ pub struct Xtask {
|
|||
pub enum XtaskCmd {
|
||||
Help(Help),
|
||||
Release(Release),
|
||||
Publish(Publish),
|
||||
Ci(Ci),
|
||||
}
|
||||
|
||||
|
@ -45,6 +60,13 @@ pub struct Help {
|
|||
#[derive(Debug)]
|
||||
pub struct Release {
|
||||
pub name: String,
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Publish {
|
||||
pub name: String,
|
||||
pub version: Version,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -3,16 +3,13 @@
|
|||
//! This binary is integrated into the `cargo` command line by using an alias in
|
||||
//! `.cargo/config`. Run commands as `cargo xtask [command]`.
|
||||
|
||||
use std::{
|
||||
env,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::{env, path::Path};
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde_json::from_str as from_json_str;
|
||||
use toml::from_str as from_toml_str;
|
||||
use xshell::read_file;
|
||||
|
||||
mod cargo;
|
||||
mod ci;
|
||||
mod flags;
|
||||
mod release;
|
||||
|
@ -30,8 +27,6 @@ fn main() {
|
|||
}
|
||||
|
||||
fn try_main() -> Result<()> {
|
||||
let project_root = project_root()?;
|
||||
|
||||
let flags = flags::Xtask::from_env()?;
|
||||
match flags.subcommand {
|
||||
flags::XtaskCmd::Help(_) => {
|
||||
|
@ -39,34 +34,35 @@ fn try_main() -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
flags::XtaskCmd::Release(cmd) => {
|
||||
let task = ReleaseTask::new(cmd.name, project_root)?;
|
||||
let mut task = ReleaseTask::new(cmd.name, cmd.version)?;
|
||||
task.run()
|
||||
}
|
||||
flags::XtaskCmd::Publish(cmd) => {
|
||||
let mut task = ReleaseTask::new(cmd.name, cmd.version)?;
|
||||
task.run()
|
||||
}
|
||||
flags::XtaskCmd::Ci(ci) => {
|
||||
let task = CiTask::new(ci.version, project_root);
|
||||
let task = CiTask::new(ci.version)?;
|
||||
task.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CargoMetadata {
|
||||
workspace_root: PathBuf,
|
||||
}
|
||||
|
||||
/// Get the project workspace root.
|
||||
fn project_root() -> Result<PathBuf> {
|
||||
let metadata_json = cmd!("cargo metadata --format-version 1").read()?;
|
||||
let metadata: CargoMetadata = from_json_str(&metadata_json)?;
|
||||
Ok(metadata.workspace_root)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Config {
|
||||
/// Credentials to authenticate to GitHub.
|
||||
github: GithubConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
/// Load a new `Config` from `config.toml`.
|
||||
fn load() -> Result<Self> {
|
||||
let path = Path::new(&env!("CARGO_MANIFEST_DIR")).join("config.toml");
|
||||
let config = read_file(path)?;
|
||||
Ok(from_toml_str(&config)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GithubConfig {
|
||||
/// The username to use for authentication.
|
||||
|
@ -76,13 +72,6 @@ struct GithubConfig {
|
|||
token: String,
|
||||
}
|
||||
|
||||
/// Load the config from `config.toml`.
|
||||
fn config() -> Result<Config> {
|
||||
let path = Path::new(&env!("CARGO_MANIFEST_DIR")).join("config.toml");
|
||||
let config = read_file(path)?;
|
||||
Ok(from_toml_str(&config)?)
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! cmd {
|
||||
($cmd:tt) => {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
io::{stdin, stdout, BufRead, Write},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
@ -10,26 +10,30 @@ use isahc::{
|
|||
http::StatusCode,
|
||||
HttpClient, ReadResponseExt, Request,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use semver::Version;
|
||||
use serde::{de::IgnoredAny, Deserialize};
|
||||
use semver::{Identifier, Version};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use toml::from_str as from_toml_str;
|
||||
use xshell::{pushd, read_file};
|
||||
|
||||
use crate::{cmd, util::ask_yes_no, GithubConfig, Result};
|
||||
use crate::{
|
||||
cargo::{Metadata, Package},
|
||||
cmd,
|
||||
util::ask_yes_no,
|
||||
GithubConfig, Result,
|
||||
};
|
||||
|
||||
const CRATESIO_API: &str = "https://crates.io/api/v1/crates";
|
||||
const GITHUB_API_RUMA: &str = "https://api.github.com/repos/ruma/ruma";
|
||||
|
||||
/// Task to create a new release of the given crate.
|
||||
#[derive(Debug)]
|
||||
pub struct ReleaseTask {
|
||||
/// The crate to release.
|
||||
local_crate: LocalCrate,
|
||||
/// The metadata of the cargo workspace.
|
||||
metadata: Metadata,
|
||||
|
||||
/// The root of the workspace.
|
||||
project_root: PathBuf,
|
||||
/// The crate to release.
|
||||
package: Package,
|
||||
|
||||
/// The new version of the crate.
|
||||
version: Version,
|
||||
|
||||
/// The http client to use for requests.
|
||||
http_client: HttpClient,
|
||||
|
@ -39,19 +43,30 @@ pub struct ReleaseTask {
|
|||
}
|
||||
|
||||
impl ReleaseTask {
|
||||
/// Create a new `ReleaseTask` with the given `name` and `project_root`.
|
||||
pub(crate) fn new(name: String, project_root: PathBuf) -> Result<Self> {
|
||||
let local_crate = LocalCrate::new(name, &project_root)?;
|
||||
let config = crate::config()?.github;
|
||||
/// Create a new `ReleaseTask` with the given `name` and `version`.
|
||||
pub(crate) fn new(name: String, version: Version) -> Result<Self> {
|
||||
let metadata = Metadata::load()?;
|
||||
|
||||
let package = metadata
|
||||
.packages
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|p| p.name == name)
|
||||
.ok_or(format!("Package {} not found in cargo metadata", name))?;
|
||||
|
||||
let config = crate::Config::load()?.github;
|
||||
|
||||
let http_client = HttpClient::new()?;
|
||||
|
||||
Ok(Self { local_crate, project_root, http_client, config })
|
||||
Ok(Self { metadata, package, version, http_client, config })
|
||||
}
|
||||
|
||||
/// Run the task to effectively create a release.
|
||||
pub(crate) fn run(self) -> Result<()> {
|
||||
pub(crate) fn run(&mut self) -> Result<()> {
|
||||
let title = &self.title();
|
||||
let prerelease = self.local_crate.version.is_prerelease();
|
||||
let prerelease = self.version.is_prerelease();
|
||||
let publish_only = self.package.name == "ruma-identifiers-validation";
|
||||
|
||||
println!(
|
||||
"Starting {} for {}…",
|
||||
match prerelease {
|
||||
|
@ -69,15 +84,41 @@ impl ReleaseTask {
|
|||
|
||||
println!("Checking status of git repository…");
|
||||
if !cmd!("git status -s -uno").read()?.is_empty()
|
||||
&& !ask_yes_no("This git repository contains untracked files. Continue?")?
|
||||
&& !ask_yes_no("This git repository contains uncommitted changes. Continue?")?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(macros) = self.macros() {
|
||||
print!("Found macros crate. ");
|
||||
let _dir = pushd(¯os.path)?;
|
||||
let published = macros.publish(&self.http_client)?;
|
||||
if self.package.version != self.version
|
||||
&& !self.package.version.is_next(&self.version)
|
||||
&& !ask_yes_no(&format!(
|
||||
"Version {} should not follow version {}. Do you really want to continue?",
|
||||
self.version, self.package.version,
|
||||
))?
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut macros = self.macros();
|
||||
|
||||
if let Some(m) = macros.as_mut() {
|
||||
println!("Found macros crate {}.", m.name);
|
||||
|
||||
m.update_version(&self.version)?;
|
||||
m.update_dependants(&self.metadata)?;
|
||||
|
||||
println!("Resuming release of {}…", self.title());
|
||||
}
|
||||
|
||||
self.package.update_version(&self.version)?;
|
||||
self.package.update_dependants(&self.metadata)?;
|
||||
|
||||
let changes = &self.package.changes(!prerelease)?;
|
||||
|
||||
self.commit()?;
|
||||
|
||||
if let Some(m) = macros {
|
||||
let published = m.publish(&self.http_client)?;
|
||||
|
||||
if published {
|
||||
// Crate was published, instead of publishing skipped (because release already
|
||||
|
@ -85,21 +126,15 @@ impl ReleaseTask {
|
|||
println!("Waiting 10 seconds for the release to make it into the crates.io index…");
|
||||
sleep(Duration::from_secs(10));
|
||||
}
|
||||
|
||||
println!("Resuming release of {}…", self.title());
|
||||
}
|
||||
|
||||
let _dir = pushd(&self.local_crate.path)?;
|
||||
self.package.publish(&self.http_client)?;
|
||||
|
||||
self.local_crate.publish(&self.http_client)?;
|
||||
|
||||
if prerelease {
|
||||
println!("Pre-release created successfully!");
|
||||
if publish_only {
|
||||
println!("Crate published successfully!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let changes = &self.local_crate.changes()?;
|
||||
|
||||
let tag = &self.tag_name();
|
||||
|
||||
println!("Creating git tag…");
|
||||
|
@ -116,6 +151,11 @@ impl ReleaseTask {
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
if prerelease {
|
||||
println!("Pre-release created successfully!");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
println!("Creating release on GitHub…");
|
||||
let request_body = &json!({
|
||||
"tag_name": tag,
|
||||
|
@ -132,13 +172,22 @@ impl ReleaseTask {
|
|||
}
|
||||
|
||||
/// Get the associated `-macros` crate of the current crate, if any.
|
||||
fn macros(&self) -> Option<LocalCrate> {
|
||||
LocalCrate::new(format!("{}-macros", self.local_crate.name), &self.project_root).ok()
|
||||
fn macros(&self) -> Option<Package> {
|
||||
self.metadata
|
||||
.packages
|
||||
.clone()
|
||||
.into_iter()
|
||||
.find(|p| p.name == format!("{}-macros", self.package.name))
|
||||
}
|
||||
|
||||
/// Get the title of this release.
|
||||
fn title(&self) -> String {
|
||||
format!("{} {}", self.local_crate.name, self.local_crate.version)
|
||||
format!("{} {}", self.package.name, self.version)
|
||||
}
|
||||
|
||||
/// Get the tag name for this release.
|
||||
fn tag_name(&self) -> String {
|
||||
format!("{}-{}", self.package.name, self.version)
|
||||
}
|
||||
|
||||
/// Load the GitHub config from the config file.
|
||||
|
@ -153,9 +202,47 @@ impl ReleaseTask {
|
|||
Ok(remote)
|
||||
}
|
||||
|
||||
/// Get the tag name for this release.
|
||||
fn tag_name(&self) -> String {
|
||||
format!("{}-{}", self.local_crate.name, self.local_crate.version)
|
||||
/// Commit and push all the changes in the git repository.
|
||||
fn commit(&self) -> Result<()> {
|
||||
let mut input = String::new();
|
||||
let stdin = stdin();
|
||||
|
||||
let instructions = "Ready to commit the changes. [continue/abort/diff]: ";
|
||||
print!("{}", instructions);
|
||||
stdout().flush()?;
|
||||
|
||||
let mut handle = stdin.lock();
|
||||
|
||||
while let _ = handle.read_line(&mut input)? {
|
||||
match input.trim().to_ascii_lowercase().as_str() {
|
||||
"c" | "continue" => {
|
||||
break;
|
||||
}
|
||||
"a" | "abort" => {
|
||||
return Err("User aborted commit".into());
|
||||
}
|
||||
"d" | "diff" => {
|
||||
cmd!("git diff").run()?;
|
||||
}
|
||||
_ => {
|
||||
println!("Unknown command.");
|
||||
}
|
||||
}
|
||||
print!("{}", instructions);
|
||||
stdout().flush()?;
|
||||
|
||||
input.clear();
|
||||
}
|
||||
|
||||
let message = format!("Release {}", self.title());
|
||||
|
||||
println!("Creating commit…");
|
||||
cmd!("git commit -a -m {message}").read()?;
|
||||
|
||||
println!("Pushing commit…");
|
||||
cmd!("git push").read()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if the tag for the current version of the crate has been pushed on GitHub.
|
||||
|
@ -187,104 +274,6 @@ 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: &Path) -> 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<bool> {
|
||||
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(false)
|
||||
} else {
|
||||
Err("Release interrupted by user.".into())
|
||||
}
|
||||
} else {
|
||||
cmd!("cargo publish").run()?;
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The required cargo manifest data of a crate.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CargoManifest {
|
||||
/// The package information.
|
||||
package: CargoPackage,
|
||||
}
|
||||
|
||||
/// The required package information from a crate's cargo manifest.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct CargoPackage {
|
||||
/// The package version.
|
||||
version: Version,
|
||||
}
|
||||
|
||||
/// A crate from the `GET /crates/{crate}` endpoint of crates.io.
|
||||
#[derive(Deserialize)]
|
||||
struct CratesIoCrate {
|
||||
version: Option<IgnoredAny>,
|
||||
}
|
||||
|
||||
/// A tag from the `GET /repos/{owner}/{repo}/tags` endpoint of GitHub REST API.
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GithubTag {
|
||||
|
@ -343,3 +332,103 @@ impl StrExt for str {
|
|||
string + s
|
||||
}
|
||||
}
|
||||
|
||||
/// Extra Version increment methods for crate release.
|
||||
trait VersionExt {
|
||||
/// Adds a pre-release label and number if there is none.
|
||||
fn add_pre_release(&mut self);
|
||||
|
||||
/// Increments the pre-release number, if this is a pre-release.
|
||||
fn increment_pre_number(&mut self);
|
||||
|
||||
/// Increments the pre-release label from `alpha` to `beta` if this is a pre-release and it is
|
||||
/// possible, otherwise does nothing.
|
||||
fn increment_pre_label(&mut self);
|
||||
|
||||
/// If the given version can be the next after this one.
|
||||
///
|
||||
/// This checks all the version bumps of the format MAJOR.MINOR.PATCH-PRE_LABEL.PRE_NUMBER, with
|
||||
/// PRE_LABEL = alpha or beta.
|
||||
fn is_next(&self, version: &Version) -> bool;
|
||||
}
|
||||
|
||||
impl VersionExt for Version {
|
||||
fn add_pre_release(&mut self) {
|
||||
if !self.is_prerelease() {
|
||||
self.pre = vec![Identifier::AlphaNumeric("alpha".into()), Identifier::Numeric(1)];
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_pre_number(&mut self) {
|
||||
if self.is_prerelease() {
|
||||
if let Identifier::Numeric(n) = self.pre[1] {
|
||||
self.pre[1] = Identifier::Numeric(n + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn increment_pre_label(&mut self) {
|
||||
if self.is_prerelease() {
|
||||
match &self.pre[0] {
|
||||
Identifier::AlphaNumeric(n) if n == "alpha" => {
|
||||
self.pre =
|
||||
vec![Identifier::AlphaNumeric("beta".into()), Identifier::Numeric(1)];
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_next(&self, version: &Version) -> bool {
|
||||
let mut next = self.clone();
|
||||
|
||||
if self.is_prerelease() {
|
||||
next.increment_pre_number();
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
|
||||
next.increment_pre_label();
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
|
||||
next.pre = vec![];
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
next.increment_patch();
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
|
||||
next.add_pre_release();
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
|
||||
next.increment_minor();
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
|
||||
next.add_pre_release();
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
|
||||
next.increment_major();
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
|
||||
next.add_pre_release();
|
||||
if next == *version {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue