From 4e2880407ce37ac61f35b88098669cdc84b528ae Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Thu, 22 Feb 2024 18:40:41 -0500 Subject: [PATCH] feat: add xtasks to assist with bumping crates --- .cargo/config.toml | 2 + .github/workflows/release.yml | 6 +- Cargo.lock | 100 ++++++++++++ Cargo.toml | 11 +- script/version | 62 -------- xtask/Cargo.toml | 20 +++ xtask/src/bump.rs | 291 ++++++++++++++++++++++++++++++++++ xtask/src/main.rs | 35 ++++ 8 files changed, 463 insertions(+), 64 deletions(-) create mode 100644 .cargo/config.toml delete mode 100755 script/version create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/bump.rs create mode 100644 xtask/src/main.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 00000000..35049cbc --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 12096836..23170819 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,7 +66,7 @@ jobs: toolchain: stable override: true - - name: Publish CLI to Crates.io + - name: Publish crates to Crates.io uses: katyo/publish-crates@v2 with: registry-token: ${{ secrets.CARGO_REGISTRY_TOKEN }} @@ -82,6 +82,10 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Build wasm + if: matrix.directory == 'lib/binding_web' + run: ./script/build-wasm + - name: Setup Node uses: actions/setup-node@v4 with: diff --git a/Cargo.lock b/Cargo.lock index 10705f95..547002ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -165,6 +165,9 @@ name = "cc" version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" +dependencies = [ + "libc", +] [[package]] name = "cesu8" @@ -522,6 +525,21 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "git2" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" +dependencies = [ + "bitflags 2.4.2", + "libc", + "libgit2-sys", + "log", + "openssl-probe", + "openssl-sys", + "url", +] + [[package]] name = "glob" version = "0.3.1" @@ -682,6 +700,20 @@ version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +[[package]] +name = "libgit2-sys" +version = "0.16.2+1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee4126d8b4ee5c9d9ea891dd875cfdc1e9d0950437179104b183d7d8a74d24e8" +dependencies = [ + "cc", + "libc", + "libssh2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", +] + [[package]] name = "libloading" version = "0.8.1" @@ -703,6 +735,32 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "libssh2-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc8a030b787e2119a731f1951d6a773e2280c660f8ec4b0f5e1505a386e71ee" +dependencies = [ + "cc", + "libc", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "libz-sys" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.13" @@ -817,6 +875,24 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.101" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -847,6 +923,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1429,6 +1511,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.4" @@ -2019,6 +2107,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "xtask" +version = "0.1.0" +dependencies = [ + "git2", + "indoc", + "semver", + "serde", + "serde_json", + "toml", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 29851308..e4b595c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,14 @@ [workspace] default-members = ["cli"] -members = ["cli", "cli/config", "cli/loader", "lib", "tags", "highlight"] +members = [ + "cli", + "cli/config", + "cli/loader", + "lib", + "tags", + "highlight", + "xtask", +] resolver = "2" [workspace.package] @@ -46,6 +54,7 @@ ctrlc = { version = "3.4.2", features = ["termination"] } difference = "2.0.0" dirs = "5.0.1" fs4 = "0.7.0" +git2 = "0.18.2" glob = "0.3.1" html-escape = "0.2.13" indexmap = "2.2.2" diff --git a/script/version b/script/version deleted file mode 100755 index ce4f6b82..00000000 --- a/script/version +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env node - -const fs = require('fs'); -const path = require('path'); -const {execFileSync} = require('child_process'); - -const cliPath = path.join(__dirname, '..', 'cli'); -const npmPath = path.join(cliPath, 'npm'); -const cargoTomlPath = path.join(cliPath, 'Cargo.toml'); - -const npmMetadata = require(path.join(npmPath, 'package.json')); -const npmVersion = npmMetadata.version; - -const cargoMetadata = fs.readFileSync(cargoTomlPath, 'utf8') -const cargoVersionMatch = cargoMetadata.match(/version = "([^"\n]+)"/); -const cargoVersion = cargoVersionMatch[1]; - -if (npmVersion !== cargoVersion) { - console.error(`NPM version ${npmVersion} does not match Cargo version ${cargoVersion}`); - process.exit(1); -} - -const arg = process.argv[2]; - -if (!arg) { - console.log([ - `Usage: script/version major | minor | patch | `, - '', - 'Update the CLI version by the given increment or to the given', - 'version number, creating a commit and tag for the new version.', - '' - ].join('\n')) - process.exit(1); -} - -if (arg) { - // Check that working directory is clean - const diff = execFileSync( - 'git', - ['diff', '--stat'], - {encoding: 'utf8'} - ); - if (diff.length !== 0) { - console.error('There are uncommitted changes.'); - process.exit(1); - } - - const newVersion = execFileSync( - 'npm', - ['version', process.argv[2], '--git-tag-version=false'], - {cwd: npmPath, encoding: 'utf8'} - ).trim().replace(/^v/, ''); - const newCargoVersionLine = cargoVersionMatch[0].replace(cargoVersion, newVersion); - const newCargoMetadata = cargoMetadata.replace(cargoVersionMatch[0], newCargoVersionLine); - fs.writeFileSync(cargoTomlPath, newCargoMetadata, 'utf8'); - execFileSync('cargo', ['build'], {cwd: cliPath}); - execFileSync('git', ['commit', '-a', '-m', newVersion]); - execFileSync('git', ['tag', 'v' + newVersion]); - console.log(newVersion) -} else { - console.log(npmVersion); -} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 00000000..4f270db2 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "xtask" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +repository.workspace = true +license.workspace = true +keywords.workspace = true +categories.workspace = true +publish = false + +[dependencies] +git2.workspace = true +indoc.workspace = true +toml.workspace = true +semver.workspace = true +serde.workspace = true +serde_json.workspace = true diff --git a/xtask/src/bump.rs b/xtask/src/bump.rs new file mode 100644 index 00000000..cd467152 --- /dev/null +++ b/xtask/src/bump.rs @@ -0,0 +1,291 @@ +use std::cmp::Ordering; +use std::path::Path; + +use git2::{DiffOptions, Repository}; +use indoc::indoc; +use semver::{BuildMetadata, Prerelease, Version}; +use toml::Value; + +fn increment_patch(v: &mut Version) { + v.patch += 1; + v.pre = Prerelease::EMPTY; + v.build = BuildMetadata::EMPTY; +} + +fn increment_minor(v: &mut Version) { + v.minor += 1; + v.patch = 0; + v.pre = Prerelease::EMPTY; + v.build = BuildMetadata::EMPTY; +} + +pub fn get_latest_tag(repo: &Repository) -> Result> { + let mut tags = repo + .tag_names(None)? + .into_iter() + .filter_map(|tag| tag.map(String::from)) + .filter_map(|tag| Version::parse(tag.strip_prefix('v').unwrap_or(&tag)).ok()) + .collect::>(); + + tags.sort_by( + |a, b| match (a.pre != Prerelease::EMPTY, b.pre != Prerelease::EMPTY) { + (true, true) | (false, false) => a.cmp(b), + (true, false) => Ordering::Less, + (false, true) => Ordering::Greater, + }, + ); + + tags.last() + .map(std::string::ToString::to_string) + .ok_or_else(|| "No tags found".into()) +} + +pub fn bump_versions() -> Result<(), Box> { + let repo = Repository::open(".")?; + let latest_tag = get_latest_tag(&repo)?; + let latest_tag_sha = repo.revparse_single(&format!("v{latest_tag}"))?.id(); + + let workspace_toml_version = fetch_workspace_version()?; + + if latest_tag != workspace_toml_version { + eprintln!( + indoc! {" + Seems like the workspace Cargo.toml ({}) version does not match up with the latest git tag ({}). + Please ensure you don't change that yourself, this subcommand will handle this for you. + "}, + workspace_toml_version, latest_tag + ); + return Ok(()); + } + + let mut revwalk = repo.revwalk()?; + revwalk.push_range(format!("{latest_tag_sha}..HEAD").as_str())?; + let mut diff_options = DiffOptions::new(); + + let current_version = Version::parse(&latest_tag)?; + let mut next_version = None; + + for oid in revwalk { + let oid = oid?; + let commit = repo.find_commit(oid)?; + let message = commit.message().unwrap(); + let message = message.trim(); + + let diff = { + let parent = commit.parent(0).unwrap(); + let parent_tree = parent.tree().unwrap(); + let commit_tree = commit.tree().unwrap(); + repo.diff_tree_to_tree( + Some(&parent_tree), + Some(&commit_tree), + Some(&mut diff_options), + )? + }; + + let mut update = false; + diff.foreach( + &mut |delta, _| { + let path = delta.new_file().path().unwrap().to_str().unwrap(); + if path.ends_with("rs") || path.ends_with("js") || path.ends_with('c') { + update = true; + } + true + }, + None, + None, + None, + )?; + + if update { + let Some((prefix, _)) = message.split_once(':') else { + continue; + }; + + let convention = if prefix.contains('(') { + prefix.split_once('(').unwrap().0 + } else { + prefix + }; + + match convention { + "feat" | "feat!" => { + let mut cur_version = current_version.clone(); + increment_minor(&mut cur_version); + if let Some(ref ver) = next_version { + if cur_version > *ver { + next_version = Some(cur_version); + } + } else { + next_version = Some(cur_version); + } + } + "fix" | "refactor" if prefix.ends_with('!') => { + let mut cur_version = current_version.clone(); + increment_minor(&mut cur_version); + if let Some(ref ver) = next_version { + if cur_version > *ver { + next_version = Some(cur_version); + } + } else { + next_version = Some(cur_version); + } + } + "fix" | "refactor" => { + let mut cur_version = current_version.clone(); + increment_patch(&mut cur_version); + if let Some(ref ver) = next_version { + if cur_version > *ver { + next_version = Some(cur_version); + } + } else { + next_version = Some(cur_version); + } + } + _ => {} + } + } + } + + if let Some(ref next_version) = next_version { + println!("Bumping from {current_version} to {next_version}"); + + update_crates(¤t_version, next_version)?; + + update_makefile(next_version)?; + + update_npm(next_version)?; + + tag_next_version(repo, next_version)?; + } + + Ok(()) +} + +fn tag_next_version( + repo: Repository, + next_version: &Version, +) -> Result<(), Box> { + // first add the manifests + + let mut index = repo.index()?; + + for file in [ + "Cargo.toml", + "cli/Cargo.toml", + "cli/config/Cargo.toml", + "cli/loader/Cargo.toml", + "lib/Cargo.toml", + "highlight/Cargo.toml", + "tags/Cargo.toml", + "cli/npm/package.json", + "lib/binding_web/package.json", + "Makefile", + ] { + index.add_path(Path::new(file))?; + } + + index.write()?; + + let tree_id = index.write_tree()?; + let tree = repo.find_tree(tree_id)?; + let signature = repo.signature()?; + let parent_commit = repo.revparse_single("HEAD")?.peel_to_commit()?; + + let commit_id = repo.commit( + Some("HEAD"), + &signature, + &signature, + &format!("{next_version}"), + &tree, + &[&parent_commit], + )?; + + let tag = repo.tag( + &format!("v{next_version}"), + &repo.find_object(commit_id, None)?, + &signature, + &format!("v{next_version}"), + false, + )?; + + println!("Tagged commit {commit_id} with tag {tag}"); + + Ok(()) +} + +fn update_makefile(next_version: &Version) -> Result<(), Box> { + let makefile = std::fs::read_to_string("Makefile")?; + let makefile = makefile + .lines() + .map(|line| { + if line.starts_with("VERSION") { + format!("VERSION := {next_version}") + } else { + line.to_string() + } + }) + .collect::>() + .join("\n") + + "\n"; + + std::fs::write("Makefile", makefile)?; + + Ok(()) +} + +fn update_crates( + current_version: &Version, + next_version: &Version, +) -> Result<(), Box> { + let mut cmd = std::process::Command::new("cargo"); + cmd.arg("workspaces").arg("version"); + + if next_version.minor > current_version.minor { + cmd.arg("minor"); + } else { + cmd.arg("patch"); + } + + cmd.arg("--no-git-commit").arg("--yes"); + + let status = cmd.status()?; + + if !status.success() { + return Err("Failed to update crates".into()); + } + + Ok(()) +} + +fn update_npm(next_version: &Version) -> Result<(), Box> { + for path in ["lib/binding_web/package.json", "cli/npm/package.json"] { + let package_json = + serde_json::from_str::(&std::fs::read_to_string(path)?)?; + + let mut package_json = package_json + .as_object() + .ok_or("Invalid package.json")? + .clone(); + package_json.insert( + "version".to_string(), + serde_json::Value::String(next_version.to_string()), + ); + + let package_json = serde_json::to_string_pretty(&package_json)? + "\n"; + + std::fs::write(path, package_json)?; + } + + Ok(()) +} + +/// read Cargo.toml and get the version +fn fetch_workspace_version() -> Result> { + let cargo_toml = toml::from_str::(&std::fs::read_to_string("Cargo.toml")?)?; + + Ok(cargo_toml["workspace"]["package"]["version"] + .as_str() + .unwrap() + .trim_matches('"') + .to_string()) +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 00000000..80bbbdd5 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,35 @@ +mod bump; + +use bump::bump_versions; + +fn print_help() { + println!( + " +xtask must specify a task to run. + +Usage: `cargo xtask ` + +Tasks: + bump-version +" + ); +} + +fn main() -> Result<(), Box> { + let Some(task) = std::env::args().nth(1) else { + print_help(); + std::process::exit(0); + }; + + match task.as_str() { + "bump-version" => { + bump_versions()?; + } + _ => { + println!("invalid task: {task}"); + std::process::exit(1); + } + } + + Ok(()) +}