xtask: Add release commit creation to release
This commit is contained in:
parent
3c237652db
commit
bc62192e60
@ -9,10 +9,10 @@ publish = false
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
isahc = { version = "1.2.0", features = ["json"] }
|
isahc = { version = "1.2.0", features = ["json"] }
|
||||||
itertools = "0.10.0"
|
|
||||||
semver = { version = "0.11.0", features = ["serde"] }
|
semver = { version = "0.11.0", features = ["serde"] }
|
||||||
serde = { version = "1.0.118", features = ["derive"] }
|
serde = { version = "1.0.118", features = ["derive"] }
|
||||||
serde_json = "1.0.60"
|
serde_json = "1.0.60"
|
||||||
toml = "0.5.8"
|
toml = "0.5.8"
|
||||||
|
toml_edit = "0.2.0"
|
||||||
xflags = "0.2.1"
|
xflags = "0.2.1"
|
||||||
xshell = "0.1.9"
|
xshell = "0.1.9"
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Ruma xtasks
|
# 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.
|
To use it, run `cargo xtask [command]` anywhere in the workspace.
|
||||||
|
|
||||||
@ -9,7 +9,10 @@ the appropriate fields.
|
|||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
- `release [crate]`: Publish `crate`, create a signed tag based on its name and version and create
|
- `release [crate] [version]`: Publish `crate` at given `version`, if applicable<sup>[1](#ref-1)</sup>, create a
|
||||||
a release on GitHub. **Requires all `github` fields in `config.toml`.**
|
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 xshell::pushd;
|
||||||
|
|
||||||
use crate::{cmd, Result};
|
use crate::{cargo::Metadata, cmd, Result};
|
||||||
|
|
||||||
const MSRV: &str = "1.45";
|
const MSRV: &str = "1.45";
|
||||||
|
|
||||||
@ -18,8 +18,9 @@ pub struct CiTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl CiTask {
|
impl CiTask {
|
||||||
pub(crate) fn new(version: Option<String>, project_root: PathBuf) -> Self {
|
pub(crate) fn new(version: Option<String>) -> Result<Self> {
|
||||||
Self { version, project_root }
|
let project_root = Metadata::load()?.workspace_root;
|
||||||
|
Ok(Self { version, project_root })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn run(self) -> Result<()> {
|
pub(crate) fn run(self) -> Result<()> {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
#![allow(dead_code)] // silence never-used warning for from_vec in generated code
|
#![allow(dead_code)] // silence never-used warning for from_vec in generated code
|
||||||
|
|
||||||
|
use semver::Version;
|
||||||
|
|
||||||
xflags::xflags! {
|
xflags::xflags! {
|
||||||
src "./src/flags.rs"
|
src "./src/flags.rs"
|
||||||
|
|
||||||
@ -14,6 +16,18 @@ xflags::xflags! {
|
|||||||
cmd release
|
cmd release
|
||||||
/// The crate to release
|
/// The crate to release
|
||||||
required name: String
|
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.
|
/// Run CI tests.
|
||||||
@ -34,6 +48,7 @@ pub struct Xtask {
|
|||||||
pub enum XtaskCmd {
|
pub enum XtaskCmd {
|
||||||
Help(Help),
|
Help(Help),
|
||||||
Release(Release),
|
Release(Release),
|
||||||
|
Publish(Publish),
|
||||||
Ci(Ci),
|
Ci(Ci),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,6 +60,13 @@ pub struct Help {
|
|||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Release {
|
pub struct Release {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub version: Version,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Publish {
|
||||||
|
pub name: String,
|
||||||
|
pub version: Version,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -3,16 +3,13 @@
|
|||||||
//! This binary is integrated into the `cargo` command line by using an alias in
|
//! This binary is integrated into the `cargo` command line by using an alias in
|
||||||
//! `.cargo/config`. Run commands as `cargo xtask [command]`.
|
//! `.cargo/config`. Run commands as `cargo xtask [command]`.
|
||||||
|
|
||||||
use std::{
|
use std::{env, path::Path};
|
||||||
env,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::from_str as from_json_str;
|
|
||||||
use toml::from_str as from_toml_str;
|
use toml::from_str as from_toml_str;
|
||||||
use xshell::read_file;
|
use xshell::read_file;
|
||||||
|
|
||||||
|
mod cargo;
|
||||||
mod ci;
|
mod ci;
|
||||||
mod flags;
|
mod flags;
|
||||||
mod release;
|
mod release;
|
||||||
@ -30,8 +27,6 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn try_main() -> Result<()> {
|
fn try_main() -> Result<()> {
|
||||||
let project_root = project_root()?;
|
|
||||||
|
|
||||||
let flags = flags::Xtask::from_env()?;
|
let flags = flags::Xtask::from_env()?;
|
||||||
match flags.subcommand {
|
match flags.subcommand {
|
||||||
flags::XtaskCmd::Help(_) => {
|
flags::XtaskCmd::Help(_) => {
|
||||||
@ -39,34 +34,35 @@ fn try_main() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
flags::XtaskCmd::Release(cmd) => {
|
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()
|
task.run()
|
||||||
}
|
}
|
||||||
flags::XtaskCmd::Ci(ci) => {
|
flags::XtaskCmd::Ci(ci) => {
|
||||||
let task = CiTask::new(ci.version, project_root);
|
let task = CiTask::new(ci.version)?;
|
||||||
task.run()
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct Config {
|
struct Config {
|
||||||
/// Credentials to authenticate to GitHub.
|
/// Credentials to authenticate to GitHub.
|
||||||
github: GithubConfig,
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GithubConfig {
|
struct GithubConfig {
|
||||||
/// The username to use for authentication.
|
/// The username to use for authentication.
|
||||||
@ -76,13 +72,6 @@ struct GithubConfig {
|
|||||||
token: String,
|
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_export]
|
||||||
macro_rules! cmd {
|
macro_rules! cmd {
|
||||||
($cmd:tt) => {
|
($cmd:tt) => {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
io::{stdin, stdout, BufRead, Write},
|
||||||
thread::sleep,
|
thread::sleep,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
@ -10,26 +10,30 @@ use isahc::{
|
|||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
HttpClient, ReadResponseExt, Request,
|
HttpClient, ReadResponseExt, Request,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use semver::{Identifier, Version};
|
||||||
use semver::Version;
|
use serde::Deserialize;
|
||||||
use serde::{de::IgnoredAny, Deserialize};
|
|
||||||
use serde_json::json;
|
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";
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct ReleaseTask {
|
pub struct ReleaseTask {
|
||||||
/// The crate to release.
|
/// The metadata of the cargo workspace.
|
||||||
local_crate: LocalCrate,
|
metadata: Metadata,
|
||||||
|
|
||||||
/// The root of the workspace.
|
/// The crate to release.
|
||||||
project_root: PathBuf,
|
package: Package,
|
||||||
|
|
||||||
|
/// The new version of the crate.
|
||||||
|
version: Version,
|
||||||
|
|
||||||
/// The http client to use for requests.
|
/// The http client to use for requests.
|
||||||
http_client: HttpClient,
|
http_client: HttpClient,
|
||||||
@ -39,19 +43,30 @@ 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 `version`.
|
||||||
pub(crate) fn new(name: String, project_root: PathBuf) -> Result<Self> {
|
pub(crate) fn new(name: String, version: Version) -> Result<Self> {
|
||||||
let local_crate = LocalCrate::new(name, &project_root)?;
|
let metadata = Metadata::load()?;
|
||||||
let config = crate::config()?.github;
|
|
||||||
|
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()?;
|
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.
|
/// 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 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!(
|
println!(
|
||||||
"Starting {} for {}…",
|
"Starting {} for {}…",
|
||||||
match prerelease {
|
match prerelease {
|
||||||
@ -69,15 +84,41 @@ impl ReleaseTask {
|
|||||||
|
|
||||||
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()
|
||||||
&& !ask_yes_no("This git repository contains untracked files. Continue?")?
|
&& !ask_yes_no("This git repository contains uncommitted changes. Continue?")?
|
||||||
{
|
{
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(macros) = self.macros() {
|
if self.package.version != self.version
|
||||||
print!("Found macros crate. ");
|
&& !self.package.version.is_next(&self.version)
|
||||||
let _dir = pushd(¯os.path)?;
|
&& !ask_yes_no(&format!(
|
||||||
let published = macros.publish(&self.http_client)?;
|
"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 {
|
if published {
|
||||||
// Crate was published, instead of publishing skipped (because release already
|
// 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…");
|
println!("Waiting 10 seconds for the release to make it into the crates.io index…");
|
||||||
sleep(Duration::from_secs(10));
|
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 publish_only {
|
||||||
|
println!("Crate published successfully!");
|
||||||
if prerelease {
|
|
||||||
println!("Pre-release created successfully!");
|
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let changes = &self.local_crate.changes()?;
|
|
||||||
|
|
||||||
let tag = &self.tag_name();
|
let tag = &self.tag_name();
|
||||||
|
|
||||||
println!("Creating git tag…");
|
println!("Creating git tag…");
|
||||||
@ -116,6 +151,11 @@ impl ReleaseTask {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if prerelease {
|
||||||
|
println!("Pre-release created successfully!");
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
println!("Creating release on GitHub…");
|
println!("Creating release on GitHub…");
|
||||||
let request_body = &json!({
|
let request_body = &json!({
|
||||||
"tag_name": tag,
|
"tag_name": tag,
|
||||||
@ -132,13 +172,22 @@ impl ReleaseTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get the associated `-macros` crate of the current crate, if any.
|
/// Get the associated `-macros` crate of the current crate, if any.
|
||||||
fn macros(&self) -> Option<LocalCrate> {
|
fn macros(&self) -> Option<Package> {
|
||||||
LocalCrate::new(format!("{}-macros", self.local_crate.name), &self.project_root).ok()
|
self.metadata
|
||||||
|
.packages
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.find(|p| p.name == format!("{}-macros", self.package.name))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the title of this release.
|
/// Get the title of this release.
|
||||||
fn title(&self) -> String {
|
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.
|
/// Load the GitHub config from the config file.
|
||||||
@ -153,9 +202,47 @@ impl ReleaseTask {
|
|||||||
Ok(remote)
|
Ok(remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the tag name for this release.
|
/// Commit and push all the changes in the git repository.
|
||||||
fn tag_name(&self) -> String {
|
fn commit(&self) -> Result<()> {
|
||||||
format!("{}-{}", self.local_crate.name, self.local_crate.version)
|
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.
|
/// 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.
|
/// A tag from the `GET /repos/{owner}/{repo}/tags` endpoint of GitHub REST API.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct GithubTag {
|
struct GithubTag {
|
||||||
@ -343,3 +332,103 @@ impl StrExt for str {
|
|||||||
string + s
|
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…
x
Reference in New Issue
Block a user