Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/socket-patch-cli/CLI_CONTRACT.md

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions crates/socket-patch-cli/src/commands/agent_download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,12 @@ pub(crate) fn format_record_skip(purl: &str, why: &str) -> String {
format!(" [skip] {} ({why})", normalize_purl(purl))
}

/// `[skip]` line for a patch a vendored run doesn't download because its
/// refusal is an expected skip (`code`, see `socket_patch_core::vendor::refusals::refusal_is_benign`).
pub(crate) fn format_refusal_skip(purl: &str, code: &str, detail: &str) -> String {
format!(" [skip] {} ({code}): {detail}", normalize_purl(purl))
}

/// Download parameters shared between get and scan commands.
pub struct DownloadParams {
pub cwd: PathBuf,
Expand Down Expand Up @@ -951,6 +957,22 @@ pub(crate) async fn fetch_selected_patches(
// fetched (nor, downstream, its pristine source). Code-tagged so a
// `--silent` operator can grep the stable code.
if let Some(VendorRefusal { code, detail }) = refusals.get(purl) {
// An expected skip (a gem version this project doesn't lock)
// is the package's whole account: no view, no record, and no
// failure, as in the vendor loop.
if socket_patch_core::vendor::refusals::refusal_is_benign(code) {
if !quiet {
eprintln!("{}", format_refusal_skip(purl, code, detail));
}
batch.events.push(tag_mode(
PatchEvent::new(EventAction::Skipped, purl)
.with_uuid(uuid)
.with_reason(*code, detail.as_str()),
batch.mode,
));
batch.skipped += 1;
continue;
}
batch.fail(
params.json,
Some(format!("[error] {purl} ({code}): {detail}")),
Expand Down
24 changes: 24 additions & 0 deletions crates/socket-patch-cli/src/commands/get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2353,6 +2353,30 @@ async fn run_get_vendored(
))
.await;
if let Some(VendorRefusal { code, detail }) = refusals.get(&patch.purl) {
// An expected skip (a gem version this project doesn't lock)
// is not an error: the same `skipped` event and `[skip]` line
// as the search path and `scan`, exit 0, nothing written.
if socket_patch_core::vendor::refusals::refusal_is_benign(code) {
if args.common.json {
env.record(tag_mode(
PatchEvent::new(EventAction::Skipped, patch.purl.as_str())
.with_uuid(patch.uuid.as_str())
.with_reason(*code, detail.as_str()),
Some("vendored"),
));
emit(&env);
} else if !args.common.silent {
eprintln!(
"{}",
crate::commands::agent_download::format_refusal_skip(
&patch.purl,
code,
detail
)
);
}
return 0;
}
// Same failure telemetry as the vendor-step Err arm below: this
// run exits 1 without vendoring anything.
socket_patch_core::telemetry::track_patch_vendor_failed(
Expand Down
35 changes: 31 additions & 4 deletions crates/socket-patch-cli/src/commands/scan/render.rs
Original file line number Diff line number Diff line change
Expand Up @@ -445,12 +445,14 @@ pub(super) enum Plan {
/// Vendored mode: of `total` selected patches, `refused` would be
/// refused by the vendored preflight (listed as `[would-refuse]`
/// lines) and `in_sync` are held by the ledger at this uuid (listed as
/// `[skip] … (already vendored)` lines); the rest are downloaded and
/// vendored.
/// `[skip] … (already vendored)` lines); `skipped` are expected skips
/// (a gem version the project doesn't lock, listed as `[skip]` lines
/// with their code); the rest are downloaded and vendored.
Vendor {
total: usize,
refused: usize,
in_sync: usize,
skipped: usize,
},
}

Expand All @@ -464,17 +466,21 @@ pub(super) fn dry_run_line(plan: Plan) -> String {
total,
refused,
in_sync,
} if refused > 0 || in_sync > 0 => {
skipped,
} if refused > 0 || in_sync > 0 || skipped > 0 => {
let mut why = Vec::new();
if in_sync > 0 {
why.push(format!("{in_sync} already vendored"));
}
if skipped > 0 {
why.push(format!("{skipped} would be skipped"));
}
if refused > 0 {
why.push(format!("{refused} would be refused"));
}
format!(
"download and vendor {} of {} ({})",
total.saturating_sub(refused + in_sync),
total.saturating_sub(refused + in_sync + skipped),
plural(total, "patch", "patches"),
why.join(", ")
)
Expand Down Expand Up @@ -995,9 +1001,30 @@ mod tests {
total,
refused,
in_sync,
skipped: 0,
}
}

/// #2075: an expected skip is not a refusal, and not work either.
#[test]
fn dry_run_line_counts_expected_skips() {
let plan = |total, refused, skipped| Plan::Vendor {
total,
refused,
in_sync: 0,
skipped,
};
assert_eq!(
dry_run_line(plan(1, 0, 1)),
"[dry-run] Would download and vendor 0 of 1 patch (1 would be skipped). No changes made."
);
assert_eq!(
dry_run_line(plan(4, 1, 2)),
"[dry-run] Would download and vendor 1 of 4 patches (2 would be skipped, \
1 would be refused). No changes made."
);
}

#[test]
fn dry_run_line_counts_refusals() {
assert_eq!(
Expand Down
43 changes: 39 additions & 4 deletions crates/socket-patch-cli/src/commands/scan/vendor_flow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,19 @@ pub(crate) enum PreviewVerdict {
WouldVendor,
}

impl PreviewVerdict {
/// For a row the wet run's preflight stops: whether it is an expected
/// skip (`[skip]`, exit 0) rather than a failure. `None` for the rest.
fn would_skip(&self) -> Option<bool> {
match self {
Self::WouldRefuse { code, .. } => {
Some(socket_patch_core::vendor::refusals::refusal_is_benign(code))
}
_ => None,
}
}
}

/// One row of [`VendorPreview`].
#[derive(Debug, Clone)]
pub(crate) struct PreviewRow {
Expand All @@ -103,11 +116,20 @@ pub(crate) struct VendorPreview {
}

impl VendorPreview {
/// How many rows the wet run would refuse.
/// How many rows the wet run would refuse as failures.
pub(crate) fn refused_count(&self) -> usize {
self.rows
.iter()
.filter(|r| matches!(r.verdict, PreviewVerdict::WouldRefuse { .. }))
.filter(|r| r.verdict.would_skip() == Some(false))
.count()
}

/// How many rows the wet run would skip without failing: a refusal
/// under a benign code.
pub(crate) fn skipped_count(&self) -> usize {
self.rows
.iter()
.filter(|r| r.verdict.would_skip() == Some(true))
.count()
}

Expand Down Expand Up @@ -199,13 +221,16 @@ impl VendorPreview {
total: self.rows.len(),
refused: self.refused_count(),
in_sync: self.in_sync_count(),
skipped: self.skipped_count(),
})
}

/// Human rendering of the preview: the headline, then a `[skip] …
/// (already vendored)` line for each in-sync row (stderr, the wet
/// run's line and stream), a `[would-refuse]` line naming what the wet
/// run would refuse and why, and the symlink advisories. Shared by
/// run would refuse and why (a `[skip]` line on stderr, as the wet run
/// prints it, when the refusal is an expected skip), and the symlink
/// advisories. Shared by
/// `scan --mode vendored --dry-run` and `get … --mode vendored
/// --dry-run` so the two cannot drift. Callers gate it on `--silent`.
pub(crate) fn print_human(&self) {
Expand All @@ -223,7 +248,17 @@ impl VendorPreview {
}
for row in &self.rows {
if let PreviewVerdict::WouldRefuse { code, detail } = &row.verdict {
println!(" [would-refuse] {} ({code}): {detail}", row.purl);
if row.verdict.would_skip() == Some(true) {
// The wet run's line and stream.
eprintln!(
"{}",
crate::commands::agent_download::format_refusal_skip(
&row.purl, code, detail
)
);
} else {
println!(" [would-refuse] {} ({code}): {detail}", row.purl);
}
}
}
for row in &self.rows {
Expand Down
21 changes: 13 additions & 8 deletions crates/socket-patch-cli/src/commands/vendor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,7 @@ pub struct VendorArgs {
pub vex: VexEmbedArgs,
}

/// Refusal codes that are expected skips, not command failures: the user's
/// request is still fully satisfied when these are the only non-successes.
fn refusal_is_benign(code: &str) -> bool {
matches!(code, "vendor_unsupported_ecosystem" | "already_vendored")
// An older vendored patch kept in force (see [`keep_older_vendored_patch`]).
|| vendor::is_unserved(code)
|| socket_patch_core::vendor::jvm::sbt_gate::SKIP_CODES.contains(&code)
}
use socket_patch_core::vendor::refusals::refusal_is_benign;

/// #954, #1235: the patch service has no artifact for `uuid` yet (still
/// building) or at all (`build_failed`, `not_found`, …) — the backend's
Expand Down Expand Up @@ -3555,6 +3548,18 @@ pub(crate) async fn vendor_records_reusing(
// revert a live hosted redirect.
CandidateVerdict::Refuse { code, detail } => {
matched.insert(candidate.clone());
if refusal_is_benign(code) {
// An expected skip, as for the backend's own
// benign refusals below.
if !common.silent && !common.json {
eprintln!("Skipping {}: {detail}", normalize_purl(candidate));
}
env.record(
PatchEvent::new(PatchAction::Skipped, candidate.clone())
.with_reason(code, detail),
);
continue;
}
has_errors = true;
env.record(
PatchEvent::new(PatchAction::Failed, candidate.clone())
Expand Down
Loading
Loading