Skip to content

Commit 3764c0b

Browse files
Wenxin-Jiangclaude
andauthored
fix: add diagnostics to apply command for silent no-op failures in CI (#53)
When `apply` runs in CI and no packages match manifest patches, it previously exited 0 silently, masking configuration errors. This change: - Tracks which manifest entries actually matched installed packages - Returns exit code 1 when targeted patches exist but nothing matched - Emits per-PURL warnings for unmatched manifest entries - Adds a post-apply summary (applied/already-patched/not-found counts) - Includes `unmatchedPatches` and `unmatchedPurls` in JSON output - Changes `apply_patches_inner` signature to return unmatched PURLs Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d25e60c commit 3764c0b

File tree

1 file changed

+60
-8
lines changed
  • crates/socket-patch-cli/src/commands

1 file changed

+60
-8
lines changed

crates/socket-patch-cli/src/commands/apply.rs

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ pub async fn run(args: ApplyArgs) -> i32 {
120120
}
121121

122122
match apply_patches_inner(&args, &manifest_path).await {
123-
Ok((success, results)) => {
123+
Ok((success, results, unmatched)) => {
124124
let patched_count = results
125125
.iter()
126126
.filter(|r| r.success && !r.files_patched.is_empty())
@@ -141,6 +141,8 @@ pub async fn run(args: ApplyArgs) -> i32 {
141141
"patchesApplied": patched_count,
142142
"alreadyPatched": already_patched_count,
143143
"failed": failed_count,
144+
"unmatchedPatches": unmatched.len(),
145+
"unmatchedPurls": unmatched,
144146
"dryRun": args.dry_run,
145147
"results": results.iter().map(result_to_json).collect::<Vec<_>>(),
146148
})).unwrap());
@@ -237,7 +239,7 @@ pub async fn run(args: ApplyArgs) -> i32 {
237239
async fn apply_patches_inner(
238240
args: &ApplyArgs,
239241
manifest_path: &Path,
240-
) -> Result<(bool, Vec<ApplyResult>), String> {
242+
) -> Result<(bool, Vec<ApplyResult>, Vec<String>), String> {
241243
let manifest = read_manifest(manifest_path)
242244
.await
243245
.map_err(|e| e.to_string())?
@@ -260,7 +262,7 @@ async fn apply_patches_inner(
260262
);
261263
eprintln!("Run \"socket-patch repair\" to download missing blobs.");
262264
}
263-
return Ok((false, Vec::new()));
265+
return Ok((false, Vec::new(), Vec::new()));
264266
}
265267

266268
if !args.silent && !args.json {
@@ -278,7 +280,7 @@ async fn apply_patches_inner(
278280
if !args.silent && !args.json {
279281
eprintln!("Some blobs could not be downloaded. Cannot apply patches.");
280282
}
281-
return Ok((false, Vec::new()));
283+
return Ok((false, Vec::new(), Vec::new()));
282284
}
283285
}
284286

@@ -287,6 +289,11 @@ async fn apply_patches_inner(
287289
let partitioned =
288290
partition_purls(&manifest_purls, args.ecosystems.as_deref());
289291

292+
let target_manifest_purls: HashSet<String> = partitioned
293+
.values()
294+
.flat_map(|purls| purls.iter().cloned())
295+
.collect();
296+
290297
let crawler_options = CrawlerOptions {
291298
cwd: args.cwd.clone(),
292299
global: args.global,
@@ -307,14 +314,20 @@ async fn apply_patches_inner(
307314
eprintln!("No package directories found");
308315
}
309316
}
310-
return Ok((false, Vec::new()));
317+
return Ok((false, Vec::new(), Vec::new()));
311318
}
312319

313320
if all_packages.is_empty() {
314321
if !args.silent && !args.json {
315-
println!("No packages found that match available patches");
322+
eprintln!("Warning: No packages found that match available patches");
323+
eprintln!(
324+
" {} targeted manifest patch(es) were in scope, but no matching packages were found on disk.",
325+
target_manifest_purls.len()
326+
);
327+
eprintln!(" Check that packages are installed and --cwd points to the right directory.");
316328
}
317-
return Ok((true, Vec::new()));
329+
let unmatched: Vec<String> = target_manifest_purls.iter().cloned().collect();
330+
return Ok((false, Vec::new(), unmatched));
318331
}
319332

320333
// Apply patches
@@ -334,6 +347,7 @@ async fn apply_patches_inner(
334347
}
335348

336349
let mut applied_base_purls: HashSet<String> = HashSet::new();
350+
let mut matched_manifest_purls: HashSet<String> = HashSet::new();
337351

338352
for (purl, pkg_path) in &all_packages {
339353
if Ecosystem::from_purl(purl) == Some(Ecosystem::Pypi) {
@@ -378,6 +392,7 @@ async fn apply_patches_inner(
378392
applied = true;
379393
applied_base_purls.insert(base_purl.clone());
380394
results.push(result);
395+
matched_manifest_purls.insert(variant_purl.clone());
381396
break;
382397
} else {
383398
results.push(result);
@@ -418,9 +433,46 @@ async fn apply_patches_inner(
418433
}
419434
}
420435
results.push(result);
436+
matched_manifest_purls.insert(purl.clone());
437+
}
438+
}
439+
440+
// Check if targeted manifest entries had no matches
441+
let unmatched: Vec<String> = target_manifest_purls
442+
.iter()
443+
.filter(|p| !matched_manifest_purls.contains(*p))
444+
.cloned()
445+
.collect();
446+
447+
if !unmatched.is_empty() && !args.silent && !args.json {
448+
eprintln!("\nWarning: {} manifest patch(es) had no matching installed package:", unmatched.len());
449+
for purl in &unmatched {
450+
eprintln!(" - {}", purl);
421451
}
422452
}
423453

454+
if !target_manifest_purls.is_empty() && matched_manifest_purls.is_empty() && !all_packages.is_empty() {
455+
if !args.silent && !args.json {
456+
eprintln!("Warning: None of the targeted manifest patches matched installed packages.");
457+
}
458+
has_errors = true;
459+
}
460+
461+
// Post-apply summary
462+
if !args.silent && !args.json {
463+
let applied_count = results.iter().filter(|r| r.success && !r.files_patched.is_empty()).count();
464+
let already_count = results.iter().filter(|r| {
465+
r.files_verified.iter().all(|f| f.status == VerifyStatus::AlreadyPatched)
466+
}).count();
467+
println!(
468+
"\nSummary: {}/{} targeted patches applied, {} already patched, {} not found on disk",
469+
applied_count,
470+
target_manifest_purls.len(),
471+
already_count,
472+
unmatched.len()
473+
);
474+
}
475+
424476
// Clean up unused blobs
425477
if !args.silent && !args.json {
426478
if let Ok(cleanup_result) = cleanup_unused_blobs(&manifest, &blobs_path, args.dry_run).await {
@@ -430,5 +482,5 @@ async fn apply_patches_inner(
430482
}
431483
}
432484

433-
Ok((!has_errors, results))
485+
Ok((!has_errors, results, unmatched))
434486
}

0 commit comments

Comments
 (0)