Skip to content

Commit 949d542

Browse files
committed
add CI workflow and local quality targets
Introduce a GitHub Actions pipeline for fmt, clippy, and test checks, plus a Makefile with matching local targets. Update README with CI badge and developer commands, and apply clippy-driven refactors so strict warning checks pass consistently.
1 parent 3f8fcbd commit 949d542

6 files changed

Lines changed: 184 additions & 110 deletions

File tree

.github/workflows/ci.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
checks:
11+
name: fmt clippy test
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup Rust
19+
uses: dtolnay/rust-toolchain@stable
20+
with:
21+
components: rustfmt, clippy
22+
23+
- name: Cache cargo artifacts
24+
uses: Swatinem/rust-cache@v2
25+
26+
- name: cargo fmt --check
27+
run: cargo fmt --all -- --check
28+
29+
- name: cargo clippy
30+
run: cargo clippy --all-targets --all-features -- -D warnings
31+
32+
- name: cargo test
33+
run: cargo test --all-targets --all-features

Makefile

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
.PHONY: fmt check clippy test run
2+
3+
fmt:
4+
cargo fmt --all
5+
6+
check:
7+
cargo fmt --all -- --check
8+
cargo check --all-targets --all-features
9+
10+
clippy:
11+
cargo clippy --all-targets --all-features -- -D warnings
12+
13+
test:
14+
cargo test --all-targets --all-features
15+
16+
run:
17+
cargo run --bin mpipe -- ask

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
![mpipe banner](images/banner/banner_readme.png)
22

33
# mpipe
4+
[![CI](https://github.com/nschaetti/mpipe/actions/workflows/ci.yml/badge.svg)](https://github.com/nschaetti/mpipe/actions/workflows/ci.yml)
5+
46
A set of LLM-based command line tools
57

68
## `mpipe ask`
@@ -163,3 +165,19 @@ OpenAI example:
163165
export OPENAI_API_KEY="..."
164166
echo "2+2?" | cargo run --quiet --bin mpipe -- ask --provider openai --model "gpt-4o-mini"
165167
```
168+
169+
## Development
170+
171+
Standard local targets:
172+
173+
```bash
174+
make check
175+
make clippy
176+
make test
177+
```
178+
179+
CI runs the same checks on `push`/`pull_request`:
180+
181+
- `cargo fmt --all -- --check`
182+
- `cargo clippy --all-targets --all-features -- -D warnings`
183+
- `cargo test --all-targets --all-features`

src/commands/ask.rs

Lines changed: 86 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,17 @@ struct UsageData {
164164
total_tokens: Option<u32>,
165165
}
166166

167+
struct VerboseContext<'a> {
168+
provider: Provider,
169+
model: &'a str,
170+
output_format: OutputFormat,
171+
dry_run: bool,
172+
show_usage: bool,
173+
prompt_source: PromptSource,
174+
messages: &'a [ChatMessage],
175+
options: &'a AskOptions,
176+
}
177+
167178
pub async fn run(cli: AskArgs) -> Result<(), String> {
168179
if cli.version {
169180
println!("{}", render_version());
@@ -200,16 +211,16 @@ pub async fn run(cli: AskArgs) -> Result<(), String> {
200211
let messages = build_messages(non_empty(system.as_deref()), &prompt);
201212

202213
if cli.verbose && !cli.quiet {
203-
log_verbose(
214+
log_verbose(VerboseContext {
204215
provider,
205-
&model,
216+
model: &model,
206217
output_format,
207-
cli.dry_run,
218+
dry_run: cli.dry_run,
208219
show_usage,
209-
main_prompt.source,
210-
&messages,
211-
&options,
212-
);
220+
prompt_source: main_prompt.source,
221+
messages: &messages,
222+
options: &options,
223+
});
213224
}
214225

215226
if cli.dry_run {
@@ -299,15 +310,15 @@ pub async fn run(cli: AskArgs) -> Result<(), String> {
299310
}
300311

301312
fn write_output(path: &Path, content: &str) -> Result<(), String> {
302-
if let Some(parent) = path.parent() {
303-
if !parent.as_os_str().is_empty() {
304-
fs::create_dir_all(parent).map_err(|err| {
305-
format!(
306-
"Failed to create output directory '{}': {err}",
307-
parent.display()
308-
)
309-
})?;
310-
}
313+
if let Some(parent) = path.parent()
314+
&& !parent.as_os_str().is_empty()
315+
{
316+
fs::create_dir_all(parent).map_err(|err| {
317+
format!(
318+
"Failed to create output directory '{}': {err}",
319+
parent.display()
320+
)
321+
})?;
311322
}
312323

313324
let now = SystemTime::now()
@@ -381,23 +392,23 @@ fn json_usage(usage: &UsageData) -> Option<JsonUsage> {
381392
}
382393

383394
fn print_usage(usage: &Option<UsageData>, latency_ms: u128) {
384-
if let Some(usage) = usage {
385-
if let Some(usage) = json_usage(usage) {
386-
eprintln!(
387-
"usage: prompt_tokens={} completion_tokens={} total_tokens={} latency_ms={}",
388-
usage
389-
.prompt_tokens
390-
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
391-
usage
392-
.completion_tokens
393-
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
394-
usage
395-
.total_tokens
396-
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
397-
latency_ms
398-
);
399-
return;
400-
}
395+
if let Some(usage) = usage
396+
&& let Some(usage) = json_usage(usage)
397+
{
398+
eprintln!(
399+
"usage: prompt_tokens={} completion_tokens={} total_tokens={} latency_ms={}",
400+
usage
401+
.prompt_tokens
402+
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
403+
usage
404+
.completion_tokens
405+
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
406+
usage
407+
.total_tokens
408+
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
409+
latency_ms
410+
);
411+
return;
401412
}
402413

403414
eprintln!("usage: unavailable latency_ms={latency_ms}");
@@ -426,18 +437,18 @@ fn build_messages(system: Option<&str>, prompt: &str) -> Vec<ChatMessage> {
426437
fn compose_prompt(preprompt: Option<&str>, main_prompt: &str, postprompt: Option<&str>) -> String {
427438
let mut parts = Vec::new();
428439

429-
if let Some(pre) = preprompt {
430-
if !pre.trim().is_empty() {
431-
parts.push(pre.to_string());
432-
}
440+
if let Some(pre) = preprompt
441+
&& !pre.trim().is_empty()
442+
{
443+
parts.push(pre.to_string());
433444
}
434445

435446
parts.push(main_prompt.to_string());
436447

437-
if let Some(post) = postprompt {
438-
if !post.trim().is_empty() {
439-
parts.push(post.to_string());
440-
}
448+
if let Some(post) = postprompt
449+
&& !post.trim().is_empty()
450+
{
451+
parts.push(post.to_string());
441452
}
442453

443454
parts.join("\n\n")
@@ -515,12 +526,12 @@ fn resolve_temperature(
515526
profile.temperature
516527
};
517528

518-
if let Some(value) = temperature {
519-
if !(0.0..=2.0).contains(&value) {
520-
return Err(format!(
521-
"Invalid temperature {value}. Must be in [0.0, 2.0]."
522-
));
523-
}
529+
if let Some(value) = temperature
530+
&& !(0.0..=2.0).contains(&value)
531+
{
532+
return Err(format!(
533+
"Invalid temperature {value}. Must be in [0.0, 2.0]."
534+
));
524535
}
525536

526537
Ok(temperature)
@@ -542,10 +553,10 @@ fn resolve_max_tokens(
542553
profile.max_tokens
543554
};
544555

545-
if let Some(value) = max_tokens {
546-
if value == 0 {
547-
return Err("Invalid max tokens 0. Must be > 0.".to_string());
548-
}
556+
if let Some(value) = max_tokens
557+
&& value == 0
558+
{
559+
return Err("Invalid max tokens 0. Must be > 0.".to_string());
549560
}
550561

551562
Ok(max_tokens)
@@ -567,10 +578,10 @@ fn resolve_timeout(
567578
profile.timeout
568579
};
569580

570-
if let Some(value) = timeout {
571-
if value == 0 {
572-
return Err("Invalid timeout 0. Must be > 0 seconds.".to_string());
573-
}
581+
if let Some(value) = timeout
582+
&& value == 0
583+
{
584+
return Err("Invalid timeout 0. Must be > 0 seconds.".to_string());
574585
}
575586

576587
Ok(timeout)
@@ -670,48 +681,43 @@ fn resolve_prompt(cli_prompt: Option<String>) -> Result<PromptInput, String> {
670681
})
671682
}
672683

673-
fn log_verbose(
674-
provider: Provider,
675-
model: &str,
676-
output_format: OutputFormat,
677-
dry_run: bool,
678-
show_usage: bool,
679-
prompt_source: PromptSource,
680-
messages: &[ChatMessage],
681-
options: &AskOptions,
682-
) {
683-
let api_key_present = provider::is_api_key_present(provider);
684-
let total_chars: usize = messages
684+
fn log_verbose(context: VerboseContext<'_>) {
685+
let api_key_present = provider::is_api_key_present(context.provider);
686+
let total_chars: usize = context
687+
.messages
685688
.iter()
686689
.map(|message| message.content.chars().count())
687690
.sum();
688691

689692
eprintln!(
690693
"verbose: provider={} endpoint={} model={} output={} dry_run={} show_usage={} prompt_source={} messages={} chars={} api_key_present={}",
691-
provider.as_str(),
692-
provider::endpoint(provider),
693-
model,
694-
output_format.as_str(),
695-
dry_run,
696-
show_usage,
697-
prompt_source.as_str(),
698-
messages.len(),
694+
context.provider.as_str(),
695+
provider::endpoint(context.provider),
696+
context.model,
697+
context.output_format.as_str(),
698+
context.dry_run,
699+
context.show_usage,
700+
context.prompt_source.as_str(),
701+
context.messages.len(),
699702
total_chars,
700703
api_key_present
701704
);
702705
eprintln!(
703706
"verbose: options temperature={} max_tokens={} timeout_secs={} retries={} retry_delay_ms={} backoff=exponential",
704-
options
707+
context
708+
.options
705709
.temperature
706710
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
707-
options
711+
context
712+
.options
708713
.max_tokens
709714
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
710-
options
715+
context
716+
.options
711717
.timeout_secs
712718
.map_or_else(|| "n/a".to_string(), |value| value.to_string()),
713-
options.retries,
714-
options.retry_delay_ms
719+
context.options.retries,
720+
context.options.retry_delay_ms
715721
);
716722
}
717723

0 commit comments

Comments
 (0)