From 4a10dde41515cff9d46e94828746f5675f4e3702 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 20 Jun 2026 20:03:40 +0000 Subject: [PATCH 1/2] fix(robot-repo-automaton): make ContentMatch detection compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `detect_content_match` read file contents with `std::fs::read` (→ `Vec`) and passed `&content` to a string `regex::Regex::is_match`, which wants `&str`. The crate therefore never compiled and the whole `DetectionMethod::ContentMatch` detection path was dead code. Switch to `std::fs::read_to_string`. This fixes the type error and also implements the "skip non-UTF8" intent already stated in the comment just above (`read_to_string` returns `Err` on non-UTF8 bytes, which the `if let Ok` then skips), matching the sibling content scan in `hypatia.rs`. Add a regression test for the previously-uncompilable path: a positive regex match, a non-match, and a non-UTF8 file that must be skipped without panicking. `cargo build` is clean and all 101 tests pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RozeeLxpJsd3WWFngaZWz3 --- robot-repo-automaton/src/detector.rs | 59 +++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/robot-repo-automaton/src/detector.rs b/robot-repo-automaton/src/detector.rs index 23b35fe9..99acea24 100644 --- a/robot-repo-automaton/src/detector.rs +++ b/robot-repo-automaton/src/detector.rs @@ -297,7 +297,9 @@ impl Detector { } } - if let Ok(content) = std::fs::read(file_path) { + // read_to_string returns Err on non-UTF8 bytes, so the + // `if let Ok` here also implements the "skip non-UTF8" intent. + if let Ok(content) = std::fs::read_to_string(file_path) { if re.is_match(&content) { affected.push(file_path.clone()); } @@ -377,6 +379,8 @@ impl Detector { #[cfg(test)] mod tests { use super::*; + use crate::catalog::{Detection, Fix, FixAction}; + use std::collections::HashMap; use tempfile::TempDir; #[test] @@ -404,4 +408,57 @@ mod tests { assert!(detector.file_exists(".github/workflows/ci.yml")); assert!(!detector.file_exists(".github/workflows/nonexistent.yml")); } + + fn content_match_error(condition: &str, files: Vec) -> ErrorType { + ErrorType { + id: "TEST-CONTENT".to_string(), + name: "content-match regression".to_string(), + severity: Severity::Medium, + category: "test".to_string(), + description: "regression coverage for detect_content_match".to_string(), + detection: Detection { + method: DetectionMethod::ContentMatch, + files, + condition: Some(condition.to_string()), + extension_map: HashMap::new(), + }, + affected_repos: vec![], + fix: Fix { + action: FixAction::Modify, + target: String::new(), + reason: None, + modification: None, + fallback: None, + }, + commit_message: "test".to_string(), + } + } + + #[test] + fn test_content_match_detects_and_skips_non_utf8() { + let temp = TempDir::new().unwrap(); + std::fs::write(temp.path().join("hit.txt"), "contains believe_me here").unwrap(); + std::fs::write(temp.path().join("miss.txt"), "nothing to see").unwrap(); + // Invalid UTF-8: detect_content_match must skip this (not panic) — the + // bug fix relies on std::fs::read_to_string returning Err here. + std::fs::write(temp.path().join("blob.bin"), [0xff, 0xfe, 0x62, 0x6d]).unwrap(); + + let detector = Detector::new(temp.path().to_path_buf()).unwrap(); + + // Positive: regex hits hit.txt; the non-UTF8 blob is skipped cleanly. + assert!( + detector + .detect(&content_match_error("believe_me", vec!["*".to_string()])) + .is_some(), + "should detect the file whose contents match the regex" + ); + + // Negative: a token present in no valid-UTF8 file yields no issue. + assert!( + detector + .detect(&content_match_error("no_such_token", vec!["*".to_string()])) + .is_none(), + "should report nothing when no file matches" + ); + } } From a30b17fcfe839ced55e707c2543bccfa91e32a93 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 21 Jun 2026 00:47:48 +0000 Subject: [PATCH 2/2] ci(rust): add build/test/clippy gate; make the crates clippy-clean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The only Rust CI was CodeQL in build-mode `none` (buildless), so nothing ever compiled or tested robot-repo-automaton / shared-context / dashboard — which is how the non-compiling content-match path reached `main` before it was fixed in the prior PR. Add `.github/workflows/rust.yml`: a per-crate matrix running `cargo build --all-targets`, `cargo test`, and `cargo clippy --all-targets -- -D warnings` (blocking), with `cargo fmt --check` informational (there is pre-existing formatting drift — ~180 hunks — that is intentionally not gated yet). To make the clippy gate pass, fix the existing findings: - fixer.rs: drop a no-op `.replace("hyperpolymath", "hyperpolymath")`. - registry_guard.rs: use `split_once('/')` instead of a manual `splitn(2)`. - exclusion_registry.rs: rename the inherent `from_str` -> `parse` (it is not a `FromStr` impl; matches `Catalog::parse`) and use `if let Ok(..)` instead of `.ok()` + `if let Some(..)`. - hypatia.rs / main.rs: accept `&Path` instead of `&PathBuf`. - Cargo.toml (robot-repo-automaton + shared-context): drop the ignored `+spec-1.1.0` build metadata from the `toml` version requirement (resolution-neutral; silences the cargo warning). - benches: import `std::hint::black_box` (criterion's re-export is deprecated). All three crates are clippy `-D warnings` clean; 101 + 84 tests pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01RozeeLxpJsd3WWFngaZWz3 --- .github/workflows/rust.yml | 47 +++++++++++++++++++ robot-repo-automaton/Cargo.toml | 2 +- .../src/exclusion_registry.rs | 8 ++-- robot-repo-automaton/src/fixer.rs | 1 - robot-repo-automaton/src/hypatia.rs | 2 +- robot-repo-automaton/src/main.rs | 4 +- robot-repo-automaton/src/registry_guard.rs | 4 +- shared-context/Cargo.toml | 2 +- shared-context/benches/fleet_benchmarks.rs | 3 +- 9 files changed, 59 insertions(+), 14 deletions(-) create mode 100644 .github/workflows/rust.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml new file mode 100644 index 00000000..f9f17e63 --- /dev/null +++ b/.github/workflows/rust.yml @@ -0,0 +1,47 @@ +# SPDX-License-Identifier: MPL-2.0 +name: Rust +# Build + test + clippy gate for the three standalone Rust crates. +# Added after a non-compiling crate (robot-repo-automaton) reached `main` +# unnoticed: the only prior Rust CI was CodeQL in build-mode `none` +# (buildless), so nothing actually compiled or tested these crates. +on: + push: + branches: [main] + pull_request: + branches: ['**'] +permissions: + contents: read +env: + CARGO_TERM_COLOR: always + # reqwest=rustls-tls, git2=vendored-openssl, gix=rust-tls. OPENSSL_NO_VENDOR + # makes openssl-sys link the runner's preinstalled system OpenSSL instead of + # recompiling the vendored copy (matches the documented local build). + OPENSSL_NO_VENDOR: '1' +jobs: + rust: + name: build · test · clippy + runs-on: ubuntu-latest + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + crate: [robot-repo-automaton, shared-context, dashboard] + defaults: + run: + working-directory: ${{ matrix.crate }} + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Ensure clippy + rustfmt components + run: rustup component add clippy rustfmt + - name: Build (all targets) + run: cargo build --all-targets --verbose + - name: Test + run: cargo test --verbose + - name: Clippy (deny warnings) + run: cargo clippy --all-targets -- -D warnings + - name: Rustfmt check (informational) + # Pre-existing formatting drift is not yet gated; surfaced here so it + # stays visible without blocking. Flip to a hard gate after a dedicated + # `cargo fmt` pass lands. + run: cargo fmt --check + continue-on-error: true diff --git a/robot-repo-automaton/Cargo.toml b/robot-repo-automaton/Cargo.toml index fb24b843..326f0f65 100644 --- a/robot-repo-automaton/Cargo.toml +++ b/robot-repo-automaton/Cargo.toml @@ -42,7 +42,7 @@ reqwest = { version = "0.12.28", features = ["json", "rustls-tls"], default-feat # Serialization serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.150" -toml = "1.1.2+spec-1.1.0" +toml = "1.1.2" # Git operations # Using vendored-openssl to avoid system OpenSSL dependency (libssl-dev) diff --git a/robot-repo-automaton/src/exclusion_registry.rs b/robot-repo-automaton/src/exclusion_registry.rs index 6b1abae4..ccf9c86f 100644 --- a/robot-repo-automaton/src/exclusion_registry.rs +++ b/robot-repo-automaton/src/exclusion_registry.rs @@ -135,11 +135,11 @@ impl ExclusionRegistry { e )) })?; - Self::from_str(&source) + Self::parse(&source) } /// Parse from an in-memory A2ML string. - pub fn from_str(source: &str) -> Result { + pub fn parse(source: &str) -> Result { let raw: RawRegistry = toml::from_str(source).map_err(|e| { crate::error::Error::Config(format!("failed to parse exclusion registry: {e}")) })?; @@ -270,7 +270,7 @@ impl ExclusionRegistry { } // Kill-switch check first — cheapest, catches everything. - if let Some(kill) = env::var("HYPATIA_AUTOMATION").ok() { + if let Ok(kill) = env::var("HYPATIA_AUTOMATION") { let k = kill.to_ascii_lowercase(); if matches!(k.as_str(), "off" | "disabled" | "0" | "false" | "halt") { return Decision::Deny { @@ -445,7 +445,7 @@ reason = "upstream homebrew" "#; fn registry() -> ExclusionRegistry { - ExclusionRegistry::from_str(FIXTURE).unwrap() + ExclusionRegistry::parse(FIXTURE).unwrap() } #[test] diff --git a/robot-repo-automaton/src/fixer.rs b/robot-repo-automaton/src/fixer.rs index 373b695e..7b55b274 100644 --- a/robot-repo-automaton/src/fixer.rs +++ b/robot-repo-automaton/src/fixer.rs @@ -618,7 +618,6 @@ impl Fixer { content .replace("gitbot-fleet", repo_name) - .replace("hyperpolymath", "hyperpolymath") .replace("{{LICENSE}}", "MPL-2.0") .replace("{{YEAR}}", &year) .replace("{{AUTHOR}}", "Jonathan D.A. Jewell") diff --git a/robot-repo-automaton/src/hypatia.rs b/robot-repo-automaton/src/hypatia.rs index 0ee3d8dd..e0ab4ba8 100644 --- a/robot-repo-automaton/src/hypatia.rs +++ b/robot-repo-automaton/src/hypatia.rs @@ -562,7 +562,7 @@ impl CicdHyperAClient { /// - Auto-approve rules that consistently produce successful fixes pub async fn report_results( &self, - repo_path: &PathBuf, + repo_path: &Path, results: &[RuleExecutionResult], ) -> crate::Result<()> { if !self.config.enable_feedback { diff --git a/robot-repo-automaton/src/main.rs b/robot-repo-automaton/src/main.rs index 822844c5..18077b07 100644 --- a/robot-repo-automaton/src/main.rs +++ b/robot-repo-automaton/src/main.rs @@ -20,7 +20,7 @@ use clap::{Parser, Subcommand}; use robot_repo_automaton::prelude::*; use robot_repo_automaton::github::{GitHubClient, CreatePullRequest}; use robot_repo_automaton::confidence::ThresholdConfig; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tracing::{debug, error, info, warn}; use tracing_subscriber::EnvFilter; @@ -712,7 +712,7 @@ async fn cmd_hooks(_config: Option<&Config>, action: HookAction) -> anyhow::Resu } /// Inspect the error catalog. -fn cmd_catalog(path: &PathBuf, severity_filter: Option<&str>) -> anyhow::Result<()> { +fn cmd_catalog(path: &Path, severity_filter: Option<&str>) -> anyhow::Result<()> { let catalog = ErrorCatalog::from_file(path)?; println!( "Error Catalog: {} error types (v{})", diff --git a/robot-repo-automaton/src/registry_guard.rs b/robot-repo-automaton/src/registry_guard.rs index 37892993..9898c8ee 100644 --- a/robot-repo-automaton/src/registry_guard.rs +++ b/robot-repo-automaton/src/registry_guard.rs @@ -152,9 +152,7 @@ fn parse_full_name(url: &str) -> Option { for prefix in ["https://", "http://", "ssh://", "git://"] { if let Some(rest) = s.strip_prefix(prefix) { // Skip the host segment. - let mut parts = rest.splitn(2, '/'); - let _host = parts.next()?; - let path = parts.next()?; + let (_host, path) = rest.split_once('/')?; return Some(path.to_string()); } } diff --git a/shared-context/Cargo.toml b/shared-context/Cargo.toml index cf43dd42..b6036241 100644 --- a/shared-context/Cargo.toml +++ b/shared-context/Cargo.toml @@ -35,7 +35,7 @@ tokio = { version = "1.52.3", features = ["sync", "fs"] } notify = "8.2.0" # Bot exclusion registry (exclusion_registry + registry_guard modules) -toml = "1.1.2+spec-1.1.0" +toml = "1.1.2" glob = "0.3.3" git2 = { version = "0.21.0", default-features = false } diff --git a/shared-context/benches/fleet_benchmarks.rs b/shared-context/benches/fleet_benchmarks.rs index 411bd4be..82ffa20a 100644 --- a/shared-context/benches/fleet_benchmarks.rs +++ b/shared-context/benches/fleet_benchmarks.rs @@ -1,8 +1,9 @@ // SPDX-License-Identifier: MPL-2.0 //! Performance benchmarks for gitbot-fleet operations -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use gitbot_shared_context::{BotId, Context, Finding, Severity}; +use std::hint::black_box; use std::path::PathBuf; /// Benchmark context creation