feat: add xtasks to assist with bumping crates

This commit is contained in:
Amaan Qureshi 2024-02-22 18:40:41 -05:00
parent e01d833d82
commit 4e2880407c
8 changed files with 463 additions and 64 deletions

2
.cargo/config.toml Normal file
View file

@ -0,0 +1,2 @@
[alias]
xtask = "run --package xtask --"

View file

@ -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:

100
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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 | <version-number>`,
'',
'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);
}

20
xtask/Cargo.toml Normal file
View file

@ -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

291
xtask/src/bump.rs Normal file
View file

@ -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<String, Box<dyn std::error::Error>> {
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::<Vec<Version>>();
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<dyn std::error::Error>> {
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(&current_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<dyn std::error::Error>> {
// 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<dyn std::error::Error>> {
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::<Vec<_>>()
.join("\n")
+ "\n";
std::fs::write("Makefile", makefile)?;
Ok(())
}
fn update_crates(
current_version: &Version,
next_version: &Version,
) -> Result<(), Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
for path in ["lib/binding_web/package.json", "cli/npm/package.json"] {
let package_json =
serde_json::from_str::<serde_json::Value>(&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<String, Box<dyn std::error::Error>> {
let cargo_toml = toml::from_str::<Value>(&std::fs::read_to_string("Cargo.toml")?)?;
Ok(cargo_toml["workspace"]["package"]["version"]
.as_str()
.unwrap()
.trim_matches('"')
.to_string())
}

35
xtask/src/main.rs Normal file
View file

@ -0,0 +1,35 @@
mod bump;
use bump::bump_versions;
fn print_help() {
println!(
"
xtask must specify a task to run.
Usage: `cargo xtask <task>`
Tasks:
bump-version
"
);
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())
}