Add xtask to automate crate releases
This commit is contained in:
parent
5b0c675cb8
commit
eb7683bae9
2
.cargo/config
Normal file
2
.cargo/config
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
[alias]
|
||||||
|
xtask = "run --package xtask --"
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
|||||||
.vscode
|
.vscode
|
||||||
target
|
target
|
||||||
Cargo.lock
|
Cargo.lock
|
||||||
|
/xtask/config.toml
|
||||||
|
@ -1,2 +1,2 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["ruma", "ruma-*"]
|
members = ["ruma", "ruma-*", "xtask"]
|
||||||
|
16
xtask/Cargo.toml
Normal file
16
xtask/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[package]
|
||||||
|
name = "xtask"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Kévin Commaille <zecakeh@pm.me>"]
|
||||||
|
edition = "2018"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
itertools = "0.10.0"
|
||||||
|
serde = { version = "1.0.118", features = ["derive"] }
|
||||||
|
serde_json = "1.0.60"
|
||||||
|
toml = "0.5.8"
|
||||||
|
xflags = "0.2.1"
|
||||||
|
xshell = "0.1.9"
|
15
xtask/README.md
Normal file
15
xtask/README.md
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
# Ruma xtasks
|
||||||
|
|
||||||
|
This crate is a helper bin for repetitive tasks during Ruma development, based on [cargo-xtask].
|
||||||
|
|
||||||
|
To use it, run `cargo xtask [command]` anywhere in the workspace.
|
||||||
|
|
||||||
|
Some commands need configuration variables. Copy `config.toml.sample` to `config.toml` and fill
|
||||||
|
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`.**
|
||||||
|
|
||||||
|
[cargo-xtask] : https://github.com/matklad/cargo-xtask
|
6
xtask/config.toml.sample
Normal file
6
xtask/config.toml.sample
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[github]
|
||||||
|
# The username to use for authentication.
|
||||||
|
user = ""
|
||||||
|
# The personal access token to use for authentication. Needs 'repo' scope.
|
||||||
|
# See: https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token
|
||||||
|
token = ""
|
48
xtask/src/flags.rs
Normal file
48
xtask/src/flags.rs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
xflags::xflags! {
|
||||||
|
src "./src/flags.rs"
|
||||||
|
|
||||||
|
/// Run custom task.
|
||||||
|
cmd xtask {
|
||||||
|
default cmd help {
|
||||||
|
/// Print help information.
|
||||||
|
optional -h, --help
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a new release of the given crate.
|
||||||
|
cmd release
|
||||||
|
required name: String
|
||||||
|
{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// generated start
|
||||||
|
// The following code is generated by `xflags` macro.
|
||||||
|
// Run `env UPDATE_XFLAGS=1 cargo build` to regenerate.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Xtask {
|
||||||
|
pub subcommand: XtaskCmd,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum XtaskCmd {
|
||||||
|
Help(Help),
|
||||||
|
Release(Release),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Help {
|
||||||
|
pub help: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Release {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Xtask {
|
||||||
|
pub const HELP: &'static str = Self::HELP_;
|
||||||
|
|
||||||
|
pub fn from_env() -> xflags::Result<Self> {
|
||||||
|
Self::from_env_()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// generated end
|
78
xtask/src/main.rs
Normal file
78
xtask/src/main.rs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
//! See <https://github.com/matklad/cargo-xtask/>.
|
||||||
|
//!
|
||||||
|
//! 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 serde::Deserialize;
|
||||||
|
use serde_json::from_str as from_json_str;
|
||||||
|
use toml::from_str as from_toml_str;
|
||||||
|
use xshell::{cmd, read_file};
|
||||||
|
|
||||||
|
mod flags;
|
||||||
|
mod release;
|
||||||
|
|
||||||
|
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
if let Err(e) = try_main() {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
std::process::exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_main() -> Result<()> {
|
||||||
|
let flags = flags::Xtask::from_env()?;
|
||||||
|
match flags.subcommand {
|
||||||
|
flags::XtaskCmd::Help(_) => {
|
||||||
|
println!("{}", flags::Xtask::HELP);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
flags::XtaskCmd::Release(cmd) => cmd.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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct GithubConfig {
|
||||||
|
/// The username to use for authentication.
|
||||||
|
user: String,
|
||||||
|
|
||||||
|
/// The personal access token to use for authentication.
|
||||||
|
token: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GithubConfig {
|
||||||
|
/// Get the GitHub credentials formatted as `user:token`
|
||||||
|
fn credentials(&self) -> String {
|
||||||
|
format!("{}:{}", self.user, self.token)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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)?)
|
||||||
|
}
|
166
xtask/src/release.rs
Normal file
166
xtask/src/release.rs
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use itertools::Itertools;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use serde_json::json;
|
||||||
|
use toml::from_str as from_toml_str;
|
||||||
|
use xshell::{cmd, pushd, read_file};
|
||||||
|
|
||||||
|
use crate::{config, flags, project_root, Result};
|
||||||
|
|
||||||
|
const GITHUB_API_RELEASES: &str = "https://api.github.com/repos/ruma/ruma/releases";
|
||||||
|
|
||||||
|
impl flags::Release {
|
||||||
|
/// Run the release command to effectively create a release.
|
||||||
|
pub(crate) fn run(self) -> Result<()> {
|
||||||
|
let project_root = &project_root()?;
|
||||||
|
let _dir = pushd(project_root.join(&self.name))?;
|
||||||
|
|
||||||
|
let remote = &self.get_remote()?;
|
||||||
|
|
||||||
|
if !cmd!("git status -s -uno").read()?.is_empty() {
|
||||||
|
return Err("This git repository contains untracked files".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let version = &self.get_version(project_root)?;
|
||||||
|
println!("Making release for {} {}…", self.name, version);
|
||||||
|
|
||||||
|
cmd!("cargo publish").run()?;
|
||||||
|
|
||||||
|
let credentials = &config()?.github.credentials();
|
||||||
|
|
||||||
|
let changes = &self.get_changes(project_root, &version)?;
|
||||||
|
|
||||||
|
let tag = &format!("{}-{}", self.name, version);
|
||||||
|
let name = &format!("{} {}", self.name, version);
|
||||||
|
|
||||||
|
cmd!("git tag -s {tag} -m {name} -m {changes}").secret(true).run()?;
|
||||||
|
|
||||||
|
cmd!("git push {remote} {tag}").run()?;
|
||||||
|
|
||||||
|
let request_body = &json!({
|
||||||
|
"tag_name": tag,
|
||||||
|
"name": name,
|
||||||
|
"body": changes.trim_softbreaks(),
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
cmd!(
|
||||||
|
"curl -u {credentials} -X POST -H 'Accept: application/vnd.github.v3+json'
|
||||||
|
{GITHUB_API_RELEASES} -d {request_body}"
|
||||||
|
)
|
||||||
|
.secret(true)
|
||||||
|
.run()?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the changes of the given version from the changelog.
|
||||||
|
fn get_changes(&self, project_root: &Path, version: &str) -> Result<String> {
|
||||||
|
let changelog = read_file(project_root.join(&self.name).join("CHANGELOG.md"))?;
|
||||||
|
let lines_nb = changelog.lines().count();
|
||||||
|
let mut lines = changelog.lines();
|
||||||
|
|
||||||
|
let start = match lines.position(|l| l.starts_with(&format!("# {}", 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())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Load the GitHub config from the config file.
|
||||||
|
fn get_remote(&self) -> Result<String> {
|
||||||
|
let branch = cmd!("git rev-parse --abbrev-ref HEAD").read()?;
|
||||||
|
let remote = cmd!("git config branch.{branch}.remote").read()?;
|
||||||
|
|
||||||
|
if remote.is_empty() {
|
||||||
|
return Err("Could not get current git remote".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(remote)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the current version of the crate from the manifest.
|
||||||
|
fn get_version(&self, project_root: &Path) -> Result<String> {
|
||||||
|
let manifest_toml = read_file(project_root.join(&self.name).join("Cargo.toml"))?;
|
||||||
|
let manifest: CargoManifest = from_toml_str(&manifest_toml)?;
|
||||||
|
|
||||||
|
Ok(manifest.package.version)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// String manipulations for crate release.
|
||||||
|
trait StrExt {
|
||||||
|
/// Remove soft line breaks as defined in CommonMark spec.
|
||||||
|
fn trim_softbreaks(&self) -> String;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrExt for str {
|
||||||
|
fn trim_softbreaks(&self) -> String {
|
||||||
|
let mut string = String::new();
|
||||||
|
let mut s = self;
|
||||||
|
|
||||||
|
while let Some(pos) = s.find('\n') {
|
||||||
|
string.push_str(&s[..pos]);
|
||||||
|
let pos_s = &s[pos..];
|
||||||
|
|
||||||
|
if pos_s.starts_with("\n\n") {
|
||||||
|
// Keep new paragraphs (multiple `\n`s).
|
||||||
|
let next = pos_s.find(|c: char| c != '\n').unwrap_or(0);
|
||||||
|
let (push, next_s) = pos_s.split_at(next);
|
||||||
|
|
||||||
|
string.push_str(push);
|
||||||
|
s = next_s;
|
||||||
|
} else if s[..pos].ends_with(" ") || s[..pos].ends_with('\\') {
|
||||||
|
// Keep hard line breaks (two spaces or a backslash before the line break).
|
||||||
|
string.push('\n');
|
||||||
|
s = &pos_s[1..];
|
||||||
|
} else if let Some(p) = pos_s.find(|c: char| !c.is_ascii_whitespace()) {
|
||||||
|
// Keep line break before list items (`\n` + whitespaces + `*` + whitespaces).
|
||||||
|
// Remove line break and keep one space otherwise.
|
||||||
|
let mut chars = pos_s.char_indices();
|
||||||
|
let (_, char) = chars.find(|(i, _)| *i == p).unwrap();
|
||||||
|
|
||||||
|
if char == '*' || char == '-' {
|
||||||
|
match chars.next() {
|
||||||
|
Some((_, next_char)) if next_char.is_ascii_whitespace() => {
|
||||||
|
string.push('\n');
|
||||||
|
s = &pos_s[1..];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string.push(' ');
|
||||||
|
s = &pos_s[p..];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string + s
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user