diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 73da0704f10..132335cc57a 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -1,6 +1,7 @@ mod ci; mod fixup; mod kotlin; +mod release; mod swift; mod workspace; @@ -8,6 +9,7 @@ use ci::CiArgs; use clap::{Parser, Subcommand}; use fixup::FixupArgs; use kotlin::KotlinArgs; +use release::ReleaseArgs; use swift::SwiftArgs; use xshell::cmd; @@ -33,6 +35,8 @@ enum Command { #[clap(long)] open: bool, }, + /// Prepare and publish a release of the matrix-sdk crates + Release(ReleaseArgs), Swift(SwiftArgs), Kotlin(KotlinArgs), } @@ -44,6 +48,7 @@ fn main() -> Result<()> { Command::Doc { open } => build_docs(open.then_some("--open"), DenyWarnings::No), Command::Swift(cfg) => cfg.run(), Command::Kotlin(cfg) => cfg.run(), + Command::Release(cfg) => cfg.run(), } } diff --git a/xtask/src/release.rs b/xtask/src/release.rs new file mode 100644 index 00000000000..d31b49b8509 --- /dev/null +++ b/xtask/src/release.rs @@ -0,0 +1,187 @@ +use std::env; + +use clap::{Args, Subcommand, ValueEnum}; +use xshell::{cmd, pushd}; + +use crate::{workspace, Result}; + +#[derive(Args)] +pub struct ReleaseArgs { + #[clap(subcommand)] + cmd: ReleaseCommand, +} + +#[derive(PartialEq, Subcommand)] +enum ReleaseCommand { + /// Prepare the release of the matrix-sdk workspace. + /// + /// This command will update the `README.md`, prepend the `CHANGELOG.md` + /// file using `git cliff`, and bump the versions in the `Cargo.toml` + /// files. + Prepare { + /// What type of version bump we should perform. + version: ReleaseVersion, + /// Actually prepare a release. Dry-run mode is the default. + #[clap(long)] + execute: bool, + }, + /// Publish the release. + /// + /// This command will create signed tags, publish the release on crates.io, + /// and finally push the tags to the repo. + Publish { + /// Actually publish a release. Dry-run mode is the default + #[clap(long)] + execute: bool, + }, + /// Get a list of interesting changes that happened in the last week. + WeeklyReport, + /// Generate the changelog for a specific crate, this shouldn't be run + /// manually, cargo-release will call this. + #[clap(hide = true)] + Changelog, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, ValueEnum)] +enum ReleaseVersion { + // TODO: Add a Major variant here once we're stable. + /// Create a new minor release. + #[default] + Minor, + /// Create a new patch release. + Patch, + /// Create a new release candidate. + Rc, +} + +impl ReleaseVersion { + fn as_str(&self) -> &str { + match self { + ReleaseVersion::Minor => "minor", + ReleaseVersion::Patch => "patch", + ReleaseVersion::Rc => "rc", + } + } +} + +impl ReleaseArgs { + pub fn run(self) -> Result<()> { + check_prerequisites(); + + // The changelog needs to be generated from the directory of the crate, + // `cargo-release` changes the directory for us but we need to + // make sure to not switch back to the workspace dir. + // + // More info: https://git-cliff.org/docs/usage/monorepos + if self.cmd != ReleaseCommand::Changelog { + let _p = pushd(workspace::root_path()?)?; + } + + match self.cmd { + ReleaseCommand::Prepare { version, execute } => prepare(version, execute), + ReleaseCommand::Publish { execute } => publish(execute), + ReleaseCommand::WeeklyReport => weekly_report(), + ReleaseCommand::Changelog => changelog(), + } + } +} + +fn check_prerequisites() { + if cmd!("cargo release --version").echo_cmd(false).ignore_stdout().run().is_err() { + eprintln!("This command requires cargo-release, please install it."); + eprintln!("More info can be found at: https://github.com/crate-ci/cargo-release?tab=readme-ov-file#install"); + + std::process::exit(1); + } + + if cmd!("git cliff --version").echo_cmd(false).ignore_stdout().run().is_err() { + eprintln!("This command requires git-cliff, please install it."); + eprintln!("More info can be found at: https://git-cliff.org/docs/installation/"); + + std::process::exit(1); + } +} + +fn prepare(version: ReleaseVersion, execute: bool) -> Result<()> { + let cmd = cmd!("cargo release --no-publish --no-tag --no-push"); + + let cmd = if execute { cmd.arg("--execute") } else { cmd }; + let cmd = cmd.arg(version.as_str()); + + cmd.run()?; + + if execute { + eprintln!( + "Please double check the changelogs and edit them if necessary, \ + publish the PR, and once it's merged, switch to `main` and pull the PR \ + and run `cargo xtask release publish`" + ); + } + + Ok(()) +} + +fn publish(execute: bool) -> Result<()> { + let cmd = cmd!("cargo release tag"); + let cmd = if execute { cmd.arg("--execute") } else { cmd }; + cmd.run()?; + + let cmd = cmd!("cargo release publish"); + let cmd = if execute { cmd.arg("--execute") } else { cmd }; + cmd.run()?; + + let cmd = cmd!("cargo release push"); + let cmd = if execute { cmd.arg("--execute") } else { cmd }; + cmd.run()?; + + Ok(()) +} + +fn weekly_report() -> Result<()> { + let lines = cmd!("git log --pretty=format:%H --since='1 week ago'").read()?; + + let Some(start) = lines.split_whitespace().last() else { + panic!("Could not find a start range for the git commit range.") + }; + + cmd!("git cliff --config cliff-weekly-report.toml {start}..HEAD").run()?; + + Ok(()) +} + +/// Generate the changelog for a given crate. +/// +/// This will be called by `cargo-release` and it will set the correct +/// environment and call it from within the correct directory. +fn changelog() -> Result<()> { + let dry_run = env::var("DRY_RUN").map(|dry| str::parse::(&dry)).unwrap_or(Ok(true))?; + let crate_name = env::var("CRATE_NAME").expect("CRATE_NAME must be set"); + let new_version = env::var("NEW_VERSION").expect("NEW_VERSION must be set"); + + if dry_run { + println!( + "\nGenerating a changelog for {} (dry run), the following output will be prepended to the CHANGELOG.md file:\n", + crate_name + ); + } else { + println!("Generating a changelog for {}.", crate_name); + } + + let command = cmd!("git cliff") + .arg("cliff") + .arg("--config") + .arg("../../cliff.toml") + .arg("--include-path") + .arg(format!("crates/{}/**/*", crate_name)) + .arg("--repository") + .arg("../../") + .arg("--unreleased") + .arg("--tag") + .arg(&new_version); + + let command = if dry_run { command } else { command.arg("--prepend").arg("CHANGELOG.md") }; + + command.run()?; + + Ok(()) +}