From aea6b9162f837297756811ba73b69c36676114b7 Mon Sep 17 00:00:00 2001 From: Yeachan-Heo Date: Thu, 2 Apr 2026 07:31:56 +0000 Subject: [PATCH] Keep Rust PRs green with a minimal CI gate Add a focused GitHub Actions workflow for pull requests into main plus manual dispatch. The workflow checks workspace formatting and runs the rusty-claude-cli crate tests so we get a real signal on the active Rust surface without widening scope into a full matrix. Because the workspace was not rustfmt-clean, include the formatting-only updates needed for the new fmt gate to pass immediately. Constraint: Keep scope to a fast, low-noise Rust PR gate Constraint: CI should validate formatting and rusty-claude-cli without expanding to full workspace coverage Rejected: Full workspace test or clippy matrix | too broad for the one-hour shipping window Rejected: Add fmt CI without reformatting the workspace | the new gate would fail on arrival Confidence: high Scope-risk: narrow Directive: Keep this workflow focused unless release requirements justify broader coverage Tested: cargo fmt --all -- --check Tested: cargo test -p rusty-claude-cli Tested: YAML parse of .github/workflows/rust-ci.yml via python3 + PyYAML Not-tested: End-to-end execution on GitHub-hosted runners --- .github/workflows/rust-ci.yml | 48 ++++++++++++++++++++++ rust/crates/api/src/providers/anthropic.rs | 5 ++- rust/crates/runtime/src/lib.rs | 12 +++--- rust/crates/runtime/src/mcp_client.rs | 10 ++--- rust/crates/rusty-claude-cli/src/main.rs | 45 +++++++++++--------- rust/crates/tools/src/lib.rs | 21 +++++++--- 6 files changed, 103 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/rust-ci.yml diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml new file mode 100644 index 0000000..f475750 --- /dev/null +++ b/.github/workflows/rust-ci.yml @@ -0,0 +1,48 @@ +name: Rust CI + +on: + pull_request: + branches: + - main + paths: + - .github/workflows/rust-ci.yml + - rust/** + workflow_dispatch: + +concurrency: + group: rust-ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +defaults: + run: + working-directory: rust + +env: + CARGO_TERM_COLOR: always + +jobs: + fmt: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + - name: Check formatting + run: cargo fmt --all -- --check + + test-rusty-claude-cli: + name: cargo test -p rusty-claude-cli + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + workspaces: rust -> target + - name: Run crate tests + run: cargo test -p rusty-claude-cli diff --git a/rust/crates/api/src/providers/anthropic.rs b/rust/crates/api/src/providers/anthropic.rs index e8cbe3f..f8b41ac 100644 --- a/rust/crates/api/src/providers/anthropic.rs +++ b/rust/crates/api/src/providers/anthropic.rs @@ -322,7 +322,10 @@ impl AnthropicClient { .with_property( "estimated_cost_usd", Value::String(format_usd( - response.usage.estimated_cost_usd(&response.model).total_cost_usd(), + response + .usage + .estimated_cost_usd(&response.model) + .total_cost_usd(), )), ), ); diff --git a/rust/crates/runtime/src/lib.rs b/rust/crates/runtime/src/lib.rs index 47e48af..6436ca6 100644 --- a/rust/crates/runtime/src/lib.rs +++ b/rust/crates/runtime/src/lib.rs @@ -24,17 +24,17 @@ pub use compact::{ get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult, }; pub use config::{ - ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpManagedProxyServerConfig, - McpConfigCollection, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, + ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection, + McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, ScopedMcpServerConfig, CLAW_SETTINGS_SCHEMA_NAME, }; pub use conversation::{ - auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, - AutoCompactionEvent, ConversationRuntime, PromptCacheEvent, RuntimeError, - StaticToolExecutor, ToolError, ToolExecutor, TurnSummary, + auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent, + ConversationRuntime, PromptCacheEvent, RuntimeError, StaticToolExecutor, ToolError, + ToolExecutor, TurnSummary, }; pub use file_ops::{ edit_file, glob_search, grep_search, read_file, write_file, EditFileOutput, GlobSearchOutput, @@ -49,7 +49,7 @@ pub use mcp::{ scoped_mcp_config_hash, unwrap_ccr_proxy_url, }; pub use mcp_client::{ - McpManagedProxyTransport, McpClientAuth, McpClientBootstrap, McpClientTransport, + McpClientAuth, McpClientBootstrap, McpClientTransport, McpManagedProxyTransport, McpRemoteTransport, McpSdkTransport, McpStdioTransport, }; pub use mcp_stdio::{ diff --git a/rust/crates/runtime/src/mcp_client.rs b/rust/crates/runtime/src/mcp_client.rs index 0e94391..e0e1f2c 100644 --- a/rust/crates/runtime/src/mcp_client.rs +++ b/rust/crates/runtime/src/mcp_client.rs @@ -97,12 +97,10 @@ impl McpClientTransport { McpServerConfig::Sdk(config) => Self::Sdk(McpSdkTransport { name: config.name.clone(), }), - McpServerConfig::ManagedProxy(config) => { - Self::ManagedProxy(McpManagedProxyTransport { - url: config.url.clone(), - id: config.id.clone(), - }) - } + McpServerConfig::ManagedProxy(config) => Self::ManagedProxy(McpManagedProxyTransport { + url: config.url.clone(), + id: config.id.clone(), + }), } } } diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 4f8362a..024774c 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -30,12 +30,11 @@ use plugins::{PluginManager, PluginManagerConfig}; use render::{MarkdownStreamState, Spinner, TerminalRenderer}; use runtime::{ clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt, - parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, - ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, - ContentBlock, ConversationMessage, ConversationRuntime, MessageRole, PromptCacheEvent, - OAuthAuthorizationRequest, OAuthConfig, - OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, RuntimeError, - Session, TokenUsage, ToolError, ToolExecutor, UsageTracker, + parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, ApiClient, + ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock, + ConversationMessage, ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig, + OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent, + RuntimeError, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker, }; use serde_json::json; use tools::GlobalToolRegistry; @@ -3146,7 +3145,8 @@ fn build_runtime( allowed_tools: Option, permission_mode: PermissionMode, progress_reporter: Option, -) -> Result, Box> { +) -> Result, Box> +{ let (feature_config, tool_registry) = build_runtime_plugin_state()?; let mut runtime = ConversationRuntime::new_with_features( session, @@ -3286,7 +3286,6 @@ impl AnthropicRuntimeClient { progress_reporter, }) } - } fn resolve_cli_auth_source() -> Result> { @@ -4023,7 +4022,9 @@ fn push_prompt_cache_record(client: &AnthropicClient, events: &mut Vec Option { +fn prompt_cache_record_to_runtime_event( + record: api::PromptCacheRecord, +) -> Option { let cache_break = record.cache_break?; Some(PromptCacheEvent { unexpected: cache_break.unexpected, @@ -4245,18 +4246,17 @@ fn print_help() { #[cfg(test)] mod tests { use super::{ - describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report, - format_internal_prompt_progress_line, format_model_report, format_model_switch_report, - format_permissions_report, + create_managed_session_handle, describe_tool_progress, filter_tool_specs, + format_compact_report, format_cost_report, format_internal_prompt_progress_line, + format_model_report, format_model_switch_report, format_permissions_report, format_permissions_switch_report, format_resume_report, format_status_report, format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args, - parse_git_status_branch, parse_git_status_metadata_for, permission_policy, - print_help_to, push_output_block, render_config_report, render_diff_report, - render_memory_report, render_repl_help, resolve_model_alias, response_to_events, + parse_git_status_branch, parse_git_status_metadata_for, permission_policy, print_help_to, + push_output_block, render_config_report, render_diff_report, render_memory_report, + render_repl_help, resolve_model_alias, resolve_session_reference, response_to_events, resume_supported_slash_commands, run_resume_command, status_context, CliAction, - CliOutputFormat, InternalPromptProgressEvent, - InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL, - create_managed_session_handle, resolve_session_reference, + CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, SlashCommand, + StatusUsage, DEFAULT_MODEL, }; use api::{MessageResponse, OutputContentBlock, Usage}; use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission}; @@ -5051,8 +5051,13 @@ mod tests { let resolved = resolve_session_reference("legacy").expect("legacy session should resolve"); assert_eq!( - resolved.path.canonicalize().expect("resolved path should exist"), - legacy_path.canonicalize().expect("legacy path should exist") + resolved + .path + .canonicalize() + .expect("resolved path should exist"), + legacy_path + .canonicalize() + .expect("legacy path should exist") ); std::env::set_current_dir(previous).expect("restore cwd"); diff --git a/rust/crates/tools/src/lib.rs b/rust/crates/tools/src/lib.rs index f37c636..96bf59a 100644 --- a/rust/crates/tools/src/lib.rs +++ b/rust/crates/tools/src/lib.rs @@ -91,7 +91,10 @@ impl GlobalToolRegistry { Ok(Self { plugin_tools }) } - pub fn normalize_allowed_tools(&self, values: &[String]) -> Result>, String> { + pub fn normalize_allowed_tools( + &self, + values: &[String], + ) -> Result>, String> { if values.is_empty() { return Ok(None); } @@ -100,7 +103,11 @@ impl GlobalToolRegistry { let canonical_names = builtin_specs .iter() .map(|spec| spec.name.to_string()) - .chain(self.plugin_tools.iter().map(|tool| tool.definition().name.clone())) + .chain( + self.plugin_tools + .iter() + .map(|tool| tool.definition().name.clone()), + ) .collect::>(); let mut name_map = canonical_names .iter() @@ -151,7 +158,8 @@ impl GlobalToolRegistry { .plugin_tools .iter() .filter(|tool| { - allowed_tools.is_none_or(|allowed| allowed.contains(tool.definition().name.as_str())) + allowed_tools + .is_none_or(|allowed| allowed.contains(tool.definition().name.as_str())) }) .map(|tool| ToolDefinition { name: tool.definition().name.clone(), @@ -174,7 +182,8 @@ impl GlobalToolRegistry { .plugin_tools .iter() .filter(|tool| { - allowed_tools.is_none_or(|allowed| allowed.contains(tool.definition().name.as_str())) + allowed_tools + .is_none_or(|allowed| allowed.contains(tool.definition().name.as_str())) }) .map(|tool| { ( @@ -2057,7 +2066,9 @@ fn push_prompt_cache_record(client: &ProviderClient, events: &mut Vec Option { +fn prompt_cache_record_to_runtime_event( + record: api::PromptCacheRecord, +) -> Option { let cache_break = record.cache_break?; Some(PromptCacheEvent { unexpected: cache_break.unexpected,