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
This commit is contained in:
Yeachan-Heo
2026-04-02 07:31:56 +00:00
parent fd0a299e19
commit aea6b9162f
6 changed files with 103 additions and 38 deletions

48
.github/workflows/rust-ci.yml vendored Normal file
View File

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

View File

@@ -322,7 +322,10 @@ impl AnthropicClient {
.with_property( .with_property(
"estimated_cost_usd", "estimated_cost_usd",
Value::String(format_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(),
)), )),
), ),
); );

View File

@@ -24,17 +24,17 @@ pub use compact::{
get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult, get_compact_continuation_message, should_compact, CompactionConfig, CompactionResult,
}; };
pub use config::{ pub use config::{
ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpManagedProxyServerConfig, ConfigEntry, ConfigError, ConfigLoader, ConfigSource, McpConfigCollection,
McpConfigCollection, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig, McpManagedProxyServerConfig, McpOAuthConfig, McpRemoteServerConfig, McpSdkServerConfig,
McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig, McpServerConfig, McpStdioServerConfig, McpTransport, McpWebSocketServerConfig, OAuthConfig,
ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig, ResolvedPermissionMode, RuntimeConfig, RuntimeFeatureConfig, RuntimeHookConfig,
RuntimePermissionRuleConfig, RuntimePluginConfig, ScopedMcpServerConfig, RuntimePermissionRuleConfig, RuntimePluginConfig, ScopedMcpServerConfig,
CLAW_SETTINGS_SCHEMA_NAME, CLAW_SETTINGS_SCHEMA_NAME,
}; };
pub use conversation::{ pub use conversation::{
auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, auto_compaction_threshold_from_env, ApiClient, ApiRequest, AssistantEvent, AutoCompactionEvent,
AutoCompactionEvent, ConversationRuntime, PromptCacheEvent, RuntimeError, ConversationRuntime, PromptCacheEvent, RuntimeError, StaticToolExecutor, ToolError,
StaticToolExecutor, ToolError, ToolExecutor, TurnSummary, ToolExecutor, TurnSummary,
}; };
pub use file_ops::{ pub use file_ops::{
edit_file, glob_search, grep_search, read_file, write_file, EditFileOutput, GlobSearchOutput, 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, scoped_mcp_config_hash, unwrap_ccr_proxy_url,
}; };
pub use mcp_client::{ pub use mcp_client::{
McpManagedProxyTransport, McpClientAuth, McpClientBootstrap, McpClientTransport, McpClientAuth, McpClientBootstrap, McpClientTransport, McpManagedProxyTransport,
McpRemoteTransport, McpSdkTransport, McpStdioTransport, McpRemoteTransport, McpSdkTransport, McpStdioTransport,
}; };
pub use mcp_stdio::{ pub use mcp_stdio::{

View File

@@ -97,12 +97,10 @@ impl McpClientTransport {
McpServerConfig::Sdk(config) => Self::Sdk(McpSdkTransport { McpServerConfig::Sdk(config) => Self::Sdk(McpSdkTransport {
name: config.name.clone(), name: config.name.clone(),
}), }),
McpServerConfig::ManagedProxy(config) => { McpServerConfig::ManagedProxy(config) => Self::ManagedProxy(McpManagedProxyTransport {
Self::ManagedProxy(McpManagedProxyTransport { url: config.url.clone(),
url: config.url.clone(), id: config.id.clone(),
id: config.id.clone(), }),
})
}
} }
} }
} }

View File

@@ -30,12 +30,11 @@ use plugins::{PluginManager, PluginManagerConfig};
use render::{MarkdownStreamState, Spinner, TerminalRenderer}; use render::{MarkdownStreamState, Spinner, TerminalRenderer};
use runtime::{ use runtime::{
clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt, clear_oauth_credentials, generate_pkce_pair, generate_state, load_system_prompt,
parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, parse_oauth_callback_request_target, resolve_sandbox_status, save_oauth_credentials, ApiClient,
ApiClient, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ApiRequest, AssistantEvent, CompactionConfig, ConfigLoader, ConfigSource, ContentBlock,
ContentBlock, ConversationMessage, ConversationRuntime, MessageRole, PromptCacheEvent, ConversationMessage, ConversationRuntime, MessageRole, OAuthAuthorizationRequest, OAuthConfig,
OAuthAuthorizationRequest, OAuthConfig, OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, PromptCacheEvent,
OAuthTokenExchangeRequest, PermissionMode, PermissionPolicy, ProjectContext, RuntimeError, RuntimeError, Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
Session, TokenUsage, ToolError, ToolExecutor, UsageTracker,
}; };
use serde_json::json; use serde_json::json;
use tools::GlobalToolRegistry; use tools::GlobalToolRegistry;
@@ -3146,7 +3145,8 @@ fn build_runtime(
allowed_tools: Option<AllowedToolSet>, allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode, permission_mode: PermissionMode,
progress_reporter: Option<InternalPromptProgressReporter>, progress_reporter: Option<InternalPromptProgressReporter>,
) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>> { ) -> Result<ConversationRuntime<AnthropicRuntimeClient, CliToolExecutor>, Box<dyn std::error::Error>>
{
let (feature_config, tool_registry) = build_runtime_plugin_state()?; let (feature_config, tool_registry) = build_runtime_plugin_state()?;
let mut runtime = ConversationRuntime::new_with_features( let mut runtime = ConversationRuntime::new_with_features(
session, session,
@@ -3286,7 +3286,6 @@ impl AnthropicRuntimeClient {
progress_reporter, progress_reporter,
}) })
} }
} }
fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> { fn resolve_cli_auth_source() -> Result<AuthSource, Box<dyn std::error::Error>> {
@@ -4023,7 +4022,9 @@ fn push_prompt_cache_record(client: &AnthropicClient, events: &mut Vec<Assistant
} }
} }
fn prompt_cache_record_to_runtime_event(record: api::PromptCacheRecord) -> Option<PromptCacheEvent> { fn prompt_cache_record_to_runtime_event(
record: api::PromptCacheRecord,
) -> Option<PromptCacheEvent> {
let cache_break = record.cache_break?; let cache_break = record.cache_break?;
Some(PromptCacheEvent { Some(PromptCacheEvent {
unexpected: cache_break.unexpected, unexpected: cache_break.unexpected,
@@ -4245,18 +4246,17 @@ fn print_help() {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ use super::{
describe_tool_progress, filter_tool_specs, format_compact_report, format_cost_report, create_managed_session_handle, describe_tool_progress, filter_tool_specs,
format_internal_prompt_progress_line, format_model_report, format_model_switch_report, format_compact_report, format_cost_report, format_internal_prompt_progress_line,
format_permissions_report, format_model_report, format_model_switch_report, format_permissions_report,
format_permissions_switch_report, format_resume_report, format_status_report, format_permissions_switch_report, format_resume_report, format_status_report,
format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args, format_tool_call_start, format_tool_result, normalize_permission_mode, parse_args,
parse_git_status_branch, parse_git_status_metadata_for, permission_policy, parse_git_status_branch, parse_git_status_metadata_for, permission_policy, print_help_to,
print_help_to, push_output_block, render_config_report, render_diff_report, push_output_block, render_config_report, render_diff_report, render_memory_report,
render_memory_report, render_repl_help, resolve_model_alias, response_to_events, render_repl_help, resolve_model_alias, resolve_session_reference, response_to_events,
resume_supported_slash_commands, run_resume_command, status_context, CliAction, resume_supported_slash_commands, run_resume_command, status_context, CliAction,
CliOutputFormat, InternalPromptProgressEvent, CliOutputFormat, InternalPromptProgressEvent, InternalPromptProgressState, SlashCommand,
InternalPromptProgressState, SlashCommand, StatusUsage, DEFAULT_MODEL, StatusUsage, DEFAULT_MODEL,
create_managed_session_handle, resolve_session_reference,
}; };
use api::{MessageResponse, OutputContentBlock, Usage}; use api::{MessageResponse, OutputContentBlock, Usage};
use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission}; use plugins::{PluginTool, PluginToolDefinition, PluginToolPermission};
@@ -5051,8 +5051,13 @@ mod tests {
let resolved = resolve_session_reference("legacy").expect("legacy session should resolve"); let resolved = resolve_session_reference("legacy").expect("legacy session should resolve");
assert_eq!( assert_eq!(
resolved.path.canonicalize().expect("resolved path should exist"), resolved
legacy_path.canonicalize().expect("legacy path should exist") .path
.canonicalize()
.expect("resolved path should exist"),
legacy_path
.canonicalize()
.expect("legacy path should exist")
); );
std::env::set_current_dir(previous).expect("restore cwd"); std::env::set_current_dir(previous).expect("restore cwd");

View File

@@ -91,7 +91,10 @@ impl GlobalToolRegistry {
Ok(Self { plugin_tools }) Ok(Self { plugin_tools })
} }
pub fn normalize_allowed_tools(&self, values: &[String]) -> Result<Option<BTreeSet<String>>, String> { pub fn normalize_allowed_tools(
&self,
values: &[String],
) -> Result<Option<BTreeSet<String>>, String> {
if values.is_empty() { if values.is_empty() {
return Ok(None); return Ok(None);
} }
@@ -100,7 +103,11 @@ impl GlobalToolRegistry {
let canonical_names = builtin_specs let canonical_names = builtin_specs
.iter() .iter()
.map(|spec| spec.name.to_string()) .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::<Vec<_>>(); .collect::<Vec<_>>();
let mut name_map = canonical_names let mut name_map = canonical_names
.iter() .iter()
@@ -151,7 +158,8 @@ impl GlobalToolRegistry {
.plugin_tools .plugin_tools
.iter() .iter()
.filter(|tool| { .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 { .map(|tool| ToolDefinition {
name: tool.definition().name.clone(), name: tool.definition().name.clone(),
@@ -174,7 +182,8 @@ impl GlobalToolRegistry {
.plugin_tools .plugin_tools
.iter() .iter()
.filter(|tool| { .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| { .map(|tool| {
( (
@@ -2057,7 +2066,9 @@ fn push_prompt_cache_record(client: &ProviderClient, events: &mut Vec<AssistantE
} }
} }
fn prompt_cache_record_to_runtime_event(record: api::PromptCacheRecord) -> Option<PromptCacheEvent> { fn prompt_cache_record_to_runtime_event(
record: api::PromptCacheRecord,
) -> Option<PromptCacheEvent> {
let cache_break = record.cache_break?; let cache_break = record.cache_break?;
Some(PromptCacheEvent { Some(PromptCacheEvent {
unexpected: cache_break.unexpected, unexpected: cache_break.unexpected,