Skip to content

Commit 9deb4c8

Browse files
committed
Keep an older vendored patch only while its wiring is still live
When a superseding patch's prebuilt artifact is pending or unavailable, the vendor run kept the older vendored patch as a benign skip whenever the ledger held a different uuid. After a relock dropped the file:.socket/vendor/... reference, that turned an unpatched package into exit 0 claiming the old patch was kept. Gate the keep on the same Discovery::vendor_entry_live verdict vendor --check and vex use, so an unwired older entry leaves the unserved upgrade a failure. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 9b29774 commit 9deb4c8

2 files changed

Lines changed: 84 additions & 4 deletions

File tree

‎crates/socket-patch-cli/src/commands/vendor.rs‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,20 @@ fn refusal_is_benign(code: &str) -> bool {
124124
/// older vendoring is still in force: like hosted mode, which keeps its pin
125125
/// and skips the upgrade, the package is a benign skip under the unserved
126126
/// code instead of a failure that would fail every re-run until the server
127-
/// builds the artifact. Any other outcome passes through, a failure minus
128-
/// the unserved marker (its error already says it).
129-
fn keep_older_vendored_patch(
127+
/// builds the artifact. Only an older vendoring whose wiring is still live
128+
/// (the [`Discovery::vendor_entry_live`] verdict `vendor --check` and `vex`
129+
/// use) is in force: once a relock dropped its `.socket/vendor/` reference
130+
/// the package is patched in neither mode, so the unserved upgrade stays a
131+
/// failure. Any other outcome passes through, a failure minus the unserved
132+
/// marker (its error already says it).
133+
///
134+
/// [`Discovery::vendor_entry_live`]: socket_patch_core::vex::discover::Discovery::vendor_entry_live
135+
async fn keep_older_vendored_patch(
130136
outcome: Option<VendorOutcome>,
131137
state: &VendorState,
132138
purl: &str,
133139
uuid: &str,
140+
common: &GlobalArgs,
134141
) -> Option<VendorOutcome> {
135142
let Some(VendorOutcome::Done {
136143
result,
@@ -149,6 +156,18 @@ fn keep_older_vendored_patch(
149156
}) {
150157
let unserved = warnings.remove(i);
151158
if let Some(kept) = lookup_entry(&state.entries, purl).filter(|e| e.uuid != uuid) {
159+
let root = common.project_root();
160+
if !crate::commands::discover_wiring(common, &root)
161+
.await
162+
.vendor_entry_live(&root, kept)
163+
.await
164+
{
165+
return Some(VendorOutcome::Done {
166+
result,
167+
entry,
168+
warnings,
169+
});
170+
}
152171
return Some(VendorOutcome::Refused {
153172
code: unserved.code,
154173
detail: format!(
@@ -3376,7 +3395,8 @@ pub(crate) async fn vendor_records_reusing(
33763395
}
33773396
}
33783397

3379-
let outcome = keep_older_vendored_patch(outcome, &state, candidate, &record.uuid);
3398+
let outcome =
3399+
keep_older_vendored_patch(outcome, &state, candidate, &record.uuid, common).await;
33803400
match outcome {
33813401
None => {
33823402
env.record(

‎crates/socket-patch-cli/tests/in_process_vendor.rs‎

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -852,6 +852,66 @@ async fn superseding_patch_without_a_served_artifact_keeps_the_vendored_one() {
852852
}
853853
}
854854

855+
/// The #954 skip keeps only an older vendoring that is still wired: once a
856+
/// relock dropped its `file:.socket/vendor/...` reference the package is
857+
/// patched in neither mode, so the unserved upgrade still fails (the
858+
/// `vendor --check` liveness verdict) instead of claiming the old patch.
859+
#[tokio::test]
860+
async fn superseding_patch_without_a_served_artifact_fails_when_the_old_one_is_unwired() {
861+
use wiremock::matchers::{method, path_regex};
862+
use wiremock::{Mock, MockServer, ResponseTemplate};
863+
const UUID2: &str = "0a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d";
864+
let fx = npm_fixture();
865+
assert_eq!(vendor_run(vendor_args(fx.root())).await, 0);
866+
// A relock: the lock no longer references the vendored artifact.
867+
std::fs::write(fx.lock_path(), &fx.original_lock).unwrap();
868+
869+
let mut manifest: Value =
870+
serde_json::from_slice(&std::fs::read(fx.manifest_path()).unwrap()).unwrap();
871+
manifest["patches"][PURL]["uuid"] = json!(UUID2);
872+
std::fs::write(
873+
fx.manifest_path(),
874+
serde_json::to_vec_pretty(&manifest).unwrap(),
875+
)
876+
.unwrap();
877+
878+
let server = MockServer::start().await;
879+
Mock::given(method("POST"))
880+
.and(path_regex(r"/(patch|patches)/package$"))
881+
.respond_with(
882+
ResponseTemplate::new(200)
883+
.set_body_json(json!({ "results": { UUID2: { "status": "pending_build" } } })),
884+
)
885+
.mount(&server)
886+
.await;
887+
let uri = server.uri();
888+
let (code, env) = vendor_cli(
889+
fx.root(),
890+
&[
891+
"--api-url",
892+
&uri,
893+
"--vendor-url",
894+
&uri,
895+
"--api-token",
896+
"sktsec_placeholder_value_for_tests_api",
897+
"--org",
898+
"acme",
899+
"--lock-timeout",
900+
"5",
901+
],
902+
);
903+
assert_eq!(code, 1, "an unwired older patch is not kept: {env:#}");
904+
let failed = find_event(&env, "failed", None);
905+
assert_eq!(failed["purl"], PURL);
906+
assert!(failed.to_string().contains("still building"), "{failed}");
907+
assert!(
908+
events(&env)
909+
.iter()
910+
.all(|e| e["errorCode"] != "vendor_prebuilt_pending"),
911+
"no unserved marker leaks out of a real failure: {env:#}"
912+
);
913+
}
914+
855915
/// The #954 skip is only for a package vendored at ANOTHER patch: a first
856916
/// vendor whose artifact is still building has nothing to keep and fails.
857917
#[tokio::test]

0 commit comments

Comments
 (0)