feat: Add a typos CI job

This commit is contained in:
Scott Schafer
2025-10-15 11:39:33 -06:00
parent 7590ff65a1
commit 6581a5da25
7 changed files with 289 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ build-man = "run --package xtask-build-man --"
stale-label = "run --package xtask-stale-label --" stale-label = "run --package xtask-stale-label --"
bump-check = "run --package xtask-bump-check --" bump-check = "run --package xtask-bump-check --"
lint-docs = "run --package xtask-lint-docs --" lint-docs = "run --package xtask-lint-docs --"
spellcheck = "run --package xtask-spellcheck --"
[env] [env]
# HACK: Until this is stabilized, `snapbox`s polyfill could get confused # HACK: Until this is stabilized, `snapbox`s polyfill could get confused

View File

@@ -28,6 +28,7 @@ jobs:
- resolver - resolver
- rustfmt - rustfmt
- schema - schema
- spellcheck
- test - test
- test_gitoxide - test_gitoxide
permissions: permissions:
@@ -304,3 +305,12 @@ jobs:
- uses: actions/checkout@v5 - uses: actions/checkout@v5
- uses: taiki-e/install-action@cargo-hack - uses: taiki-e/install-action@cargo-hack
- run: cargo hack check --all-targets --rust-version --workspace --ignore-private --locked - run: cargo hack check --all-targets --rust-version --workspace --ignore-private --locked
spellcheck:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v5
- name: Spell Check Repo
uses: crate-ci/typos@v1.38.1

11
Cargo.lock generated
View File

@@ -5211,6 +5211,17 @@ dependencies = [
"itertools 0.14.0", "itertools 0.14.0",
] ]
[[package]]
name = "xtask-spellcheck"
version = "0.0.0"
dependencies = [
"anyhow",
"cargo-util",
"cargo_metadata",
"clap",
"semver",
]
[[package]] [[package]]
name = "xtask-stale-label" name = "xtask-stale-label"
version = "0.0.0" version = "0.0.0"

View File

@@ -0,0 +1,15 @@
[package]
name = "xtask-spellcheck"
version = "0.0.0"
edition.workspace = true
publish = false
[dependencies]
anyhow.workspace = true
cargo_metadata.workspace = true
cargo-util.workspace = true
clap.workspace = true
semver.workspace = true
[lints]
workspace = true

View File

@@ -0,0 +1,202 @@
#![allow(clippy::disallowed_methods)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
use anyhow::Result;
use cargo_metadata::{Metadata, MetadataCommand};
use clap::{Arg, ArgAction};
use semver::Version;
use std::{
env, io,
path::{Path, PathBuf},
process::Command,
};
const BIN_NAME: &str = "typos";
const PKG_NAME: &str = "typos-cli";
const TYPOS_STEP_PREFIX: &str = " uses: crate-ci/typos@v";
fn main() -> anyhow::Result<()> {
let cli = cli();
exec(&cli.get_matches())?;
Ok(())
}
pub fn cli() -> clap::Command {
clap::Command::new("xtask-spellcheck")
.arg(
Arg::new("color")
.long("color")
.help("Coloring: auto, always, never")
.action(ArgAction::Set)
.value_name("WHEN")
.global(true),
)
.arg(
Arg::new("quiet")
.long("quiet")
.short('q')
.help("Do not print cargo log messages")
.action(ArgAction::SetTrue)
.global(true),
)
.arg(
Arg::new("verbose")
.long("verbose")
.short('v')
.help("Use verbose output (-vv very verbose/build.rs output)")
.action(ArgAction::Count)
.global(true),
)
.arg(
Arg::new("write-changes")
.long("write-changes")
.short('w')
.help("Write fixes out")
.action(ArgAction::SetTrue)
.global(true),
)
}
pub fn exec(matches: &clap::ArgMatches) -> Result<()> {
let mut args = vec![];
match matches.get_one::<String>("color") {
Some(c) if matches!(c.as_str(), "auto" | "always" | "never") => {
args.push("--color");
args.push(c);
}
Some(c) => {
anyhow::bail!(
"argument for --color must be auto, always, or \
never, but found `{}`",
c
);
}
_ => {}
}
if matches.get_flag("quiet") {
args.push("--quiet");
}
let verbose_count = matches.get_count("verbose");
for _ in 0..verbose_count {
args.push("--verbose");
}
if matches.get_flag("write-changes") {
args.push("--write-changes");
}
let metadata = MetadataCommand::new()
.exec()
.expect("cargo_metadata failed");
let required_version = extract_workflow_typos_version(&metadata)?;
let outdir = metadata
.build_directory
.unwrap_or_else(|| metadata.target_directory)
.as_std_path()
.join("tmp");
let workspace_root = metadata.workspace_root.as_path().as_std_path();
let bin_path = crate::ensure_version_or_cargo_install(&outdir, required_version)?;
eprintln!("running {BIN_NAME}");
Command::new(bin_path)
.current_dir(workspace_root)
.args(args)
.status()?;
Ok(())
}
fn extract_workflow_typos_version(metadata: &Metadata) -> anyhow::Result<Version> {
let ws_root = metadata.workspace_root.as_path().as_std_path();
let workflow_path = ws_root.join(".github").join("workflows").join("main.yml");
let file_content = std::fs::read_to_string(workflow_path)?;
if let Some(line) = file_content
.lines()
.find(|line| line.contains(TYPOS_STEP_PREFIX))
&& let Some(stripped) = line.strip_prefix(TYPOS_STEP_PREFIX)
&& let Ok(v) = Version::parse(stripped)
{
Ok(v)
} else {
Err(anyhow::anyhow!("Could not find typos version in workflow"))
}
}
/// If the given executable is installed with the given version, use that,
/// otherwise install via cargo.
pub fn ensure_version_or_cargo_install(
build_dir: &Path,
required_version: Version,
) -> io::Result<PathBuf> {
// Check if the user has a sufficient version already installed
let bin_path = PathBuf::from(BIN_NAME).with_extension(env::consts::EXE_EXTENSION);
if let Some(user_version) = get_typos_version(&bin_path) {
if user_version >= required_version {
return Ok(bin_path);
}
}
let tool_root_dir = build_dir.join("misc-tools");
let tool_bin_dir = tool_root_dir.join("bin");
let bin_path = tool_bin_dir
.join(BIN_NAME)
.with_extension(env::consts::EXE_EXTENSION);
// Check if we have already installed sufficient version
if let Some(misc_tools_version) = get_typos_version(&bin_path) {
if misc_tools_version >= required_version {
return Ok(bin_path);
}
}
eprintln!("required `typos` version ({required_version}) not found, building from source");
let mut cmd = Command::new("cargo");
// use --force to ensure that if the required version is bumped, we update it.
cmd.args(["install", "--locked", "--force", "--quiet"])
.arg("--root")
.arg(&tool_root_dir)
// use --target-dir to ensure we have a build cache so repeated invocations aren't slow.
.arg("--target-dir")
.arg(tool_root_dir.join("target"))
.arg(format!("{PKG_NAME}@{required_version}"))
// modify PATH so that cargo doesn't print a warning telling the user to modify the path.
.env(
"PATH",
env::join_paths(
env::split_paths(&env::var("PATH").unwrap())
.chain(std::iter::once(tool_bin_dir.clone())),
)
.expect("build dir contains invalid char"),
);
let cargo_exit_code = cmd.spawn()?.wait()?;
if !cargo_exit_code.success() {
return Err(io::Error::other("cargo install failed"));
}
assert!(
matches!(bin_path.try_exists(), Ok(true)),
"cargo install did not produce the expected binary"
);
eprintln!("finished {BIN_NAME}");
Ok(bin_path)
}
fn get_typos_version(bin: &PathBuf) -> Option<Version> {
// ignore the process exit code here and instead just let the version number check fail
if let Ok(output) = Command::new(&bin).arg("--version").output()
&& let Ok(s) = String::from_utf8(output.stdout)
&& let Some(version_str) = s.trim().split_whitespace().last()
{
Version::parse(version_str).ok()
} else {
None
}
}

View File

@@ -99,6 +99,11 @@ Some guidelines on working on a change:
* Include tests that cover all non-trivial code. See the [Testing chapter] for * Include tests that cover all non-trivial code. See the [Testing chapter] for
more about writing and running tests. more about writing and running tests.
* All code should be warning-free. This is checked during tests. * All code should be warning-free. This is checked during tests.
* All changes should be free of typos. Cargo's CI has a job that runs [`typos`]
to enforce this. You can use `cargo spellcheck` to run this check locally,
and `cargo spellcheck --write-changes` to fix most typos automatically.
[`typos`]: https://github.com/crate-ci/typos
## Submitting a Pull Request ## Submitting a Pull Request

45
typos.toml Executable file
View File

@@ -0,0 +1,45 @@
[files]
extend-exclude = [
"crates/resolver-tests/*",
"LICENSE-THIRD-PARTY",
"tests/testsuite/script/rustc_fixtures",
]
[default]
extend-ignore-re = [
# Handles ssh keys
"AAAA[0-9A-Za-z+/]+[=]{0,3}",
# Handles paseto from login tests
"k3[.](secret|public)[.][a-zA-Z0-9_-]+",
]
extend-ignore-identifiers-re = [
# Handles git short SHA-1 hashes
"[a-f0-9]{8,9}",
]
extend-ignore-words-re = [
# words with length <= 4 chars is likely noise
"^[a-zA-Z]{1,4}$",
]
[default.extend-identifiers]
# This comes from `windows_sys`
ERROR_FILENAME_EXCED_RANGE = "ERROR_FILENAME_EXCED_RANGE"
# Name of a dependency
flate2 = "flate2"
[default.extend-words]
filetimes = "filetimes"
[type.cargo_command]
extend-glob = ["cargo_command.rs"]
[type.cargo_command.extend-words]
biuld = "biuld"
[type.random-sample]
extend-glob = ["random-sample"]
[type.random-sample.extend-words]
objekt = "objekt"