From 8129622130de444e52d41169aac830856f72e777 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 19:23:05 +0000 Subject: [PATCH 1/3] Start fix for #1295 Assisted-by: Claude Code:claude-opus-5-5 From ccf2c0264423f78a1e020c77306452c56d22485e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 19:29:39 +0000 Subject: [PATCH 2/3] Warn about stale v4 Bundler plugin registrations v5 removed setup --remove, which cleared Bundler's machine-local plugin registration in .bundle/plugin/index. Checkouts that ran bundle install under v4 kept pointing at the deleted .socket/bundler-plugin/, so Bundler 2.3-2.5 fail every install with a LoadError and newer versions warn on each run. scan and apply on gem projects now report such a registration as gem_bundler_plugin_stale with the one-line fix, and the migration guide, CLI contract and retired-setup error name the per-checkout bundle plugin uninstall step. Fixes #1295 Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-cli/CLI_CONTRACT.md | 11 + crates/socket-patch-cli/src/commands/apply.rs | 14 +- .../socket-patch-cli/src/commands/scan/mod.rs | 14 +- crates/socket-patch-cli/src/lib.rs | 5 +- .../tests/apply/gem_stale_bundler_plugin.rs | 163 ++++++++++++++ crates/socket-patch-cli/tests/apply/main.rs | 1 + .../socket-patch-cli/tests/cli_parse_main.rs | 6 + .../src/crawlers/ruby_crawler.rs | 202 ++++++++++++++++++ .../src/patch/redirect/upstream/mod.rs | 5 +- docs/migrating-to-v5.md | 2 +- 10 files changed, 418 insertions(+), 5 deletions(-) create mode 100644 crates/socket-patch-cli/tests/apply/gem_stale_bundler_plugin.rs diff --git a/crates/socket-patch-cli/CLI_CONTRACT.md b/crates/socket-patch-cli/CLI_CONTRACT.md index bb4592f4b..a774564f4 100644 --- a/crates/socket-patch-cli/CLI_CONTRACT.md +++ b/crates/socket-patch-cli/CLI_CONTRACT.md @@ -455,6 +455,17 @@ hand (the `postinstall`/`dependencies` entries, the `socket-patch[hook]` depende `post-install-cmd`/`post-update-cmd` entries). The `socket-patch-hook` wheel and the `socket-patch-bundler` gem are no longer published. +Bundler also keeps a machine-local registration of the plugin in the uncommitted +`.bundle/plugin/index`, which v4's `setup --remove` cleared and nothing in v5 does. Every +checkout that ran `bundle install` under v4 must run `bundle plugin uninstall socket-patch` (or +delete `.bundle/plugin/`) once: with the plugin directory gone, Bundler 2.3–2.5 fail every +`bundle install` with a `LoadError` and 2.6+ warn on each run. `scan` (gem ecosystem selected, +project mode) and `apply` (gem patches in scope, project mode) detect a registration of +`socket-patch` at a path that no longer exists and report it as a `gem_bundler_plugin_stale` +run-level `warnings[]` entry under `--json` (detail names the registered path, the index file and +the remedy), and as one stderr `Warning: …` line otherwise (`apply` gates it on `!--silent`). A +registration whose directory still exists is a working v4 setup and is not reported. + Prefer hosted or vendored mode: their lockfile (and `.socket/vendor/`) edits are the persistence, so no Socket Patch install hook is needed. Agent mode (`scan --mode agent`, `get --mode agent`, `apply`) patches the installed tree in place, which the next package-manager install reverts; wire it into CI yourself: diff --git a/crates/socket-patch-cli/src/commands/apply.rs b/crates/socket-patch-cli/src/commands/apply.rs index 22ed44888..66c9053a9 100644 --- a/crates/socket-patch-cli/src/commands/apply.rs +++ b/crates/socket-patch-cli/src/commands/apply.rs @@ -1,7 +1,9 @@ use clap::Args; use socket_patch_core::api::blob_fetcher::get_missing_blobs; use socket_patch_core::api::client::{get_api_client_with_overrides, ApiClient}; -use socket_patch_core::crawlers::ruby_crawler::config_path_ignored_warning; +use socket_patch_core::crawlers::ruby_crawler::{ + config_path_ignored_warning, stale_plugin_registration_warning, +}; use socket_patch_core::crawlers::{ bun_uses_global_store, detect_npm_pkg_manager, Ecosystem, NpmPkgManager, RubyCrawler, }; @@ -2227,6 +2229,16 @@ async fn apply_patches_inner( detail, }); } + // A Bundler plugin registration v4's `setup` left in this checkout, + // pointing at a plugin dir the v5 migration deleted (#1295). + if gem_discovery.is_some() { + if let Some((code, detail)) = stale_plugin_registration_warning(&args.common.cwd).await { + run_warnings.push(RunWarning { + code: code.to_string(), + detail, + }); + } + } let mut fallback_skips: Vec = Vec::new(); // Multi-copy aware: npm nests genuine duplicates of one `name@version` diff --git a/crates/socket-patch-cli/src/commands/scan/mod.rs b/crates/socket-patch-cli/src/commands/scan/mod.rs index a3d5b08e6..518fd8074 100644 --- a/crates/socket-patch-cli/src/commands/scan/mod.rs +++ b/crates/socket-patch-cli/src/commands/scan/mod.rs @@ -13,7 +13,9 @@ use socket_patch_core::api::client::{ is_fallback_candidate, ApiClient, ApiError, }; use socket_patch_core::api::types::{BatchPackagePatches, BatchSearchResponse, PatchSearchResult}; -use socket_patch_core::crawlers::ruby_crawler::config_path_ignored_warning; +use socket_patch_core::crawlers::ruby_crawler::{ + config_path_ignored_warning, stale_plugin_registration_warning, +}; use socket_patch_core::crawlers::Ecosystem; use socket_patch_core::manifest::schema::PatchManifest; use socket_patch_core::telemetry::{ @@ -1853,6 +1855,16 @@ async fn run_scan( layout_refusals.push((code.to_string(), detail)); } } + // A Bundler plugin registration v4's `setup` left in this checkout, + // pointing at a plugin dir the v5 migration deleted (#1295). + if args.common.ecosystem_selected(Ecosystem::Gem) + && !args.common.global + && args.common.global_prefix.is_none() + { + if let Some((code, detail)) = stale_plugin_registration_warning(&args.common.cwd).await { + layout_refusals.push((code.to_string(), detail)); + } + } // Supplement purls, captured for the path-scope filter below: their // `path` fields are fabricated placeholders, so a path-scoped scan // excludes them (with a counted warning) instead of glob-matching diff --git a/crates/socket-patch-cli/src/lib.rs b/crates/socket-patch-cli/src/lib.rs index 5f6336f21..3ea8768e4 100644 --- a/crates/socket-patch-cli/src/lib.rs +++ b/crates/socket-patch-cli/src/lib.rs @@ -334,7 +334,10 @@ const RETIRED_SUBCOMMANDS: &[(&str, &str)] = &[ "was removed in v5.0, together with the install hooks it wired. In CI, run \ `socket-patch apply` after each install (agent mode), or switch to \ `socket-patch scan --mode hosted` or `--mode vendored`, whose lockfile edits \ - need no hook", + need no hook. To remove the old Bundler plugin, delete the Gemfile \ + `plugin \"socket-patch\"` block and `.socket/bundler-plugin/`, then run \ + `bundle plugin uninstall socket-patch` in every checkout that ran \ + `bundle install` with it", ), ( "unlock", diff --git a/crates/socket-patch-cli/tests/apply/gem_stale_bundler_plugin.rs b/crates/socket-patch-cli/tests/apply/gem_stale_bundler_plugin.rs new file mode 100644 index 000000000..52e38dba6 --- /dev/null +++ b/crates/socket-patch-cli/tests/apply/gem_stale_bundler_plugin.rs @@ -0,0 +1,163 @@ +//! #1295: a Bundler plugin registration v4's `setup` left in a checkout. +//! +//! v4's `setup --remove` cleared `.bundle/plugin/index`; v5 has no +//! `setup`, and `.bundle/` is never committed, so every other checkout +//! that ran `bundle install` under v4 still registers `socket-patch` at +//! the `.socket/bundler-plugin/` the migration commit deleted. Bundler +//! 2.3-2.5 then fail every `bundle install` with a `LoadError`. scan and +//! apply on a gem project must say so, with the one-line fix. + +use crate::common::{git_sha256, run_with_env}; + +use std::path::Path; + +const PURL: &str = "pkg:gem/rack@3.1.0"; +const CODE: &str = "gem_bundler_plugin_stale"; + +/// A Gemfile project whose `.bundle/plugin/index` registers +/// `socket-patch` the way Bundler writes it after a v4 `setup` + +/// `bundle install`. `.socket/bundler-plugin/` exists only when +/// `plugin_dir_present`. With `with_patch`, a `vendor/bundle` store holds +/// rack@3.1.0 and the manifest carries a patch for it. +fn build_project(root: &Path, plugin_dir_present: bool, with_patch: bool) -> String { + std::fs::write(root.join("Gemfile"), b"source 'https://rubygems.org'\n").unwrap(); + let plugin_dir = root.join(".socket").join("bundler-plugin"); + let dir = plugin_dir.display().to_string(); + let index_dir = root.join(".bundle").join("plugin"); + std::fs::create_dir_all(&index_dir).unwrap(); + std::fs::write( + index_dir.join("index"), + format!( + "---\ncommands:\nhooks:\n before-install-all:\n - \"socket-patch\"\n\ + load_paths:\n socket-patch:\n - \"{dir}/lib\"\nplugin_paths:\n \ + socket-patch: \"{dir}\"\nsources:\n" + ), + ) + .unwrap(); + if plugin_dir_present { + std::fs::create_dir_all(&plugin_dir).unwrap(); + } + if with_patch { + let original = b"module Rack\n VERSION = 'VULNERABLE'\nend\n"; + let mut patched = original.to_vec(); + patched.extend_from_slice(b"# SOCKET-PATCHED\n"); + let before_hash = git_sha256(original); + let after_hash = git_sha256(&patched); + let gem_lib = root.join("vendor/bundle/ruby/3.2.0/gems/rack-3.1.0/lib"); + std::fs::create_dir_all(&gem_lib).unwrap(); + std::fs::write(gem_lib.join("rack.rb"), original).unwrap(); + let socket = root.join(".socket"); + std::fs::create_dir_all(socket.join("blobs")).unwrap(); + std::fs::write( + socket.join("manifest.json"), + format!( + r#"{{ "patches": {{ + "{PURL}": {{ + "uuid": "636f6e66-6967-4761-8264-000000001295", + "exportedAt": "2024-01-01T00:00:00Z", + "files": {{ "lib/rack.rb": {{ + "beforeHash": "{before_hash}", "afterHash": "{after_hash}" + }}}}, + "vulnerabilities": {{}}, "description": "stale-plugin fixture", + "license": "MIT", "tier": "free" + }} + }}}}"# + ), + ) + .unwrap(); + std::fs::write(socket.join("blobs").join(&after_hash), &patched).unwrap(); + } + dir +} + +/// Run with no `gem` binary on PATH and the app config dir pinned to the +/// project's own `.bundle/`, so no ambient Bundler state is read. +fn run(root: &Path, args: &[&str]) -> (i32, String, String) { + let empty_path = root.join("empty-bin"); + std::fs::create_dir_all(&empty_path).unwrap(); + let app_config = root.join(".bundle"); + let mut argv: Vec<&str> = args.to_vec(); + let cwd = root.display().to_string(); + argv.extend(["--cwd", cwd.as_str()]); + run_with_env( + root, + &argv, + &[ + ("PATH", empty_path.to_str().unwrap()), + ("BUNDLE_APP_CONFIG", app_config.to_str().unwrap()), + ], + ) +} + +fn stale_warning(env: &serde_json::Value) -> Option { + env.get("warnings")? + .as_array()? + .iter() + .find(|w| w.get("code").and_then(|c| c.as_str()) == Some(CODE)) + .and_then(|w| w.get("detail")?.as_str().map(str::to_string)) +} + +#[test] +fn scan_json_warns_about_stale_plugin_registration() { + let tmp = tempfile::tempdir().unwrap(); + let dir = build_project(tmp.path(), false, false); + let (code, stdout, stderr) = run(tmp.path(), &["scan", "--json", "--yes"]); + assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); + let env: serde_json::Value = serde_json::from_str(stdout.trim()) + .unwrap_or_else(|e| panic!("scan must emit JSON: {e}; stdout={stdout}")); + let detail = stale_warning(&env) + .unwrap_or_else(|| panic!("warnings[] must carry {CODE}.\nenvelope: {env}")); + assert!(detail.contains(&dir), "detail names the path: {detail}"); + assert!( + detail.contains("bundle plugin uninstall socket-patch"), + "detail carries the remedy: {detail}" + ); +} + +/// A v4 setup that is still wired loads fine: no warning. +#[test] +fn scan_json_is_quiet_while_the_plugin_dir_exists() { + let tmp = tempfile::tempdir().unwrap(); + build_project(tmp.path(), true, false); + let (code, stdout, stderr) = run(tmp.path(), &["scan", "--json", "--yes"]); + assert_eq!(code, 0, "stdout:\n{stdout}\nstderr:\n{stderr}"); + let env: serde_json::Value = serde_json::from_str(stdout.trim()).unwrap(); + assert_eq!(stale_warning(&env), None, "envelope: {env}"); +} + +#[test] +fn apply_warns_about_stale_plugin_registration() { + let tmp = tempfile::tempdir().unwrap(); + let dir = build_project(tmp.path(), false, true); + let (code, stdout, stderr) = run( + tmp.path(), + &["apply", "--json", "--offline", "--ecosystems", "gem"], + ); + let env: serde_json::Value = serde_json::from_str(stdout.trim()) + .unwrap_or_else(|e| panic!("apply must emit JSON: {e}; stdout={stdout}")); + assert_eq!(code, 0, "envelope: {env}\nstderr:\n{stderr}"); + assert_eq!(env["summary"]["applied"], 1, "envelope: {env}"); + let detail = stale_warning(&env) + .unwrap_or_else(|| panic!("warnings[] must carry {CODE}.\nenvelope: {env}")); + assert!(detail.contains(&dir), "detail names the path: {detail}"); + + // Human path: one stderr line, gated on --silent. + let (code, _out, stderr) = run(tmp.path(), &["apply", "--offline", "--ecosystems", "gem"]); + assert_eq!(code, 0, "stderr:\n{stderr}"); + assert_eq!( + stderr + .matches("Warning: Bundler still registers the removed v4 socket-patch plugin") + .count(), + 1, + "stderr:\n{stderr}" + ); + let (code, _out, stderr) = run( + tmp.path(), + &["apply", "--offline", "--ecosystems", "gem", "--silent"], + ); + assert_eq!(code, 0, "stderr:\n{stderr}"); + assert!( + !stderr.contains("Bundler still registers"), + "stderr:\n{stderr}" + ); +} diff --git a/crates/socket-patch-cli/tests/apply/main.rs b/crates/socket-patch-cli/tests/apply/main.rs index 3de89ed38..edfaa50b0 100644 --- a/crates/socket-patch-cli/tests/apply/main.rs +++ b/crates/socket-patch-cli/tests/apply/main.rs @@ -16,6 +16,7 @@ mod check_verifies_installed_tree; mod cli_gem_variant_mismatch_policy; mod covgap_commands_apply; mod e2e_safety_advisories; +mod gem_stale_bundler_plugin; mod in_process_gem_config_warning; mod in_process_gem_fallback_home; mod in_process_gem_multicopy; diff --git a/crates/socket-patch-cli/tests/cli_parse_main.rs b/crates/socket-patch-cli/tests/cli_parse_main.rs index 1663cda27..9310736fa 100644 --- a/crates/socket-patch-cli/tests/cli_parse_main.rs +++ b/crates/socket-patch-cli/tests/cli_parse_main.rs @@ -171,6 +171,12 @@ fn setup_subcommand_is_removed() { assert!(text.contains("removed in v5.0"), "{text}"); assert!(text.contains("socket-patch apply"), "{text}"); assert!(!text.contains("self-update"), "{text}"); + // #1295: removing the Bundler plugin by hand also needs the + // per-checkout deregistration `setup --remove` used to do. + assert!( + text.contains("bundle plugin uninstall socket-patch"), + "{text}" + ); } #[test] diff --git a/crates/socket-patch-core/src/crawlers/ruby_crawler.rs b/crates/socket-patch-core/src/crawlers/ruby_crawler.rs index ae366ad86..7cca22484 100644 --- a/crates/socket-patch-core/src/crawlers/ruby_crawler.rs +++ b/crates/socket-patch-core/src/crawlers/ruby_crawler.rs @@ -1335,6 +1335,103 @@ pub fn config_path_ignored_warning(value: &str) -> (&'static str, String) { ) } +/// The name v4's `socket-patch setup` registered its Bundler plugin under. +const SOCKET_BUNDLER_PLUGIN: &str = "socket-patch"; + +/// The stable warning `(code, detail)` for a Bundler plugin registration +/// that v4's `setup` left behind and v5 can no longer remove (#1295). +/// +/// v4's `setup --remove` cleared `.bundle/plugin/index` (#210); v5 dropped +/// `setup`, and `.bundle/` is never committed, so every checkout that ran +/// `bundle install` under v4 keeps a registration pointing at the deleted +/// `.socket/bundler-plugin/`. Bundler 2.3–2.5 then abort every +/// `bundle install` with a `LoadError`, and 2.6+ warn on each run. The +/// crawler stays print-free: scan and apply surface it on their own +/// warning channels, as with [`config_path_ignored_warning`]. +/// +/// `None` unless the app config dir's `plugin/index` registers +/// `socket-patch` at a path that no longer exists. A registration whose +/// directory is still there is a v4 setup that is still wired, which +/// Bundler loads fine. +pub async fn stale_plugin_registration_warning(root: &Path) -> Option<(&'static str, String)> { + stale_plugin_registration_warning_with_env( + root, + std::env::var_os("BUNDLE_APP_CONFIG").as_deref(), + ) + .await +} + +pub(crate) async fn stale_plugin_registration_warning_with_env( + root: &Path, + app_config_env: Option<&OsStr>, +) -> Option<(&'static str, String)> { + let index = bundler_app_config_dir(root, app_config_env) + .join("plugin") + .join("index"); + let text = crate::utils::fs::read_regular_to_string(&index) + .await + .ok()?; + let registered = registered_plugin_path(&text, SOCKET_BUNDLER_PLUGIN)?; + let path = Path::new(®istered); + let path = if path.is_absolute() { + path.to_path_buf() + } else { + root.join(path) + }; + if crate::utils::fs::file_exists(&path).await { + return None; + } + Some(( + "gem_bundler_plugin_stale", + format!( + "Bundler still registers the removed v4 socket-patch plugin at \"{registered}\" \ + (in {}); Bundler 2.3-2.5 fail every `bundle install` with a LoadError and \ + newer versions warn on each run. Run `bundle plugin uninstall socket-patch` \ + in this checkout (or delete its .bundle/plugin directory) to clear it", + index.display() + ), + )) +} + +/// The path `name` is registered at under `plugin_paths:` in a Bundler +/// plugin index. Bundler writes the index with its own YAML serializer +/// (`Bundler::YAMLSerializer`): top-level keys at column 0, one +/// ` name: "path"` line per plugin beneath, so a line scan reads it +/// exactly. Quotes are optional, for indexes written by other tools. +fn registered_plugin_path(index: &str, name: &str) -> Option { + fn unquote(s: &str) -> &str { + let s = s.trim(); + for q in ['"', '\''] { + if let Some(inner) = s.strip_prefix(q).and_then(|t| t.strip_suffix(q)) { + return inner; + } + } + s + } + let mut in_section = false; + for line in index.lines() { + let line = line.trim_end(); + if line.is_empty() { + continue; + } + if !line.starts_with([' ', '\t']) { + in_section = line == "plugin_paths:"; + continue; + } + if !in_section { + continue; + } + let Some((key, value)) = line.trim_start().split_once(": ") else { + continue; + }; + if unquote(key) == name { + let value = unquote(value); + return (!value.is_empty()).then(|| value.to_string()); + } + } + None +} + /// The ambient home directory ([`home_dir`]) as an env value — the /// `~`-expansion base for ambient runs; tests inject theirs through the /// `_with_env` seams. @@ -3332,6 +3429,111 @@ mod tests { ); } + /// A `.bundle/plugin/index` as Bundler 4.0.18 writes it after + /// `bundle install` with v4's managed `plugin "socket-patch", path:` + /// block, with `dir` standing for the registered plugin directory. + fn v4_plugin_index(dir: &str) -> String { + format!( + "---\ncommands:\nhooks:\n before-install-all:\n - \"socket-patch\"\n\ + load_paths:\n socket-patch:\n - \"{dir}/lib\"\nplugin_paths:\n \ + socket-patch: \"{dir}\"\nsources:\n" + ) + } + + #[test] + fn registered_plugin_path_reads_bundlers_index() { + let index = v4_plugin_index("/app/.socket/bundler-plugin"); + assert_eq!( + registered_plugin_path(&index, "socket-patch").as_deref(), + Some("/app/.socket/bundler-plugin") + ); + assert_eq!(registered_plugin_path(&index, "other"), None); + // CRLF, unquoted values and a different plugin before ours. + let crlf = "---\r\nplugin_paths:\r\n other: /x\r\n socket-patch: /y\r\n"; + assert_eq!( + registered_plugin_path(crlf, "socket-patch").as_deref(), + Some("/y") + ); + // `bundle plugin uninstall socket-patch` leaves the keys empty. + let cleared = "---\ncommands:\nhooks:\nload_paths:\nplugin_paths:\nsources:\n"; + assert_eq!(registered_plugin_path(cleared, "socket-patch"), None); + // The name under another section is not a registration. + let elsewhere = "---\nload_paths:\n socket-patch: \"/z\"\nplugin_paths:\n"; + assert_eq!(registered_plugin_path(elsewhere, "socket-patch"), None); + } + + /// #1295: v5 has no `setup --remove`, so a checkout that ran + /// `bundle install` under v4 keeps the registration after the + /// migration commit deletes `.socket/bundler-plugin/`. That stale + /// registration (and only that) must produce the warning. + #[tokio::test] + async fn stale_plugin_registration_is_reported() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + let plugin_dir = root.join(".socket").join("bundler-plugin"); + let index_dir = root.join(".bundle").join("plugin"); + std::fs::create_dir_all(&index_dir).unwrap(); + let dir = plugin_dir.display().to_string(); + std::fs::write(index_dir.join("index"), v4_plugin_index(&dir)).unwrap(); + + // Still wired (v4 setup in place): Bundler loads it fine. + std::fs::create_dir_all(&plugin_dir).unwrap(); + assert_eq!( + stale_plugin_registration_warning_with_env(root, None).await, + None + ); + + // The migration commit removed the plugin dir: stale. + std::fs::remove_dir_all(&plugin_dir).unwrap(); + let (code, detail) = stale_plugin_registration_warning_with_env(root, None) + .await + .expect("a registration at a missing path must warn"); + assert_eq!(code, "gem_bundler_plugin_stale"); + assert!(detail.contains(&dir), "detail names the path: {detail}"); + assert!( + detail.contains("bundle plugin uninstall socket-patch"), + "detail carries the remedy: {detail}" + ); + + // After `bundle plugin uninstall socket-patch`: nothing to report. + std::fs::write( + index_dir.join("index"), + "---\ncommands:\nhooks:\nload_paths:\nplugin_paths:\nsources:\n", + ) + .unwrap(); + assert_eq!( + stale_plugin_registration_warning_with_env(root, None).await, + None + ); + } + + /// The index lives under the app config dir, so a `BUNDLE_APP_CONFIG` + /// that moves it is followed, and the default `.bundle/` is not read. + #[tokio::test] + async fn stale_plugin_registration_follows_bundle_app_config() { + let tmp = tempfile::tempdir().unwrap(); + let root = tmp.path(); + let missing = root.join(".socket").join("bundler-plugin"); + let index_dir = root.join("cfg").join("plugin"); + std::fs::create_dir_all(&index_dir).unwrap(); + std::fs::write( + index_dir.join("index"), + v4_plugin_index(&missing.display().to_string()), + ) + .unwrap(); + assert_eq!( + stale_plugin_registration_warning_with_env(root, None).await, + None, + "no index under the default .bundle/" + ); + assert!( + stale_plugin_registration_warning_with_env(root, Some(OsStr::new("cfg"))) + .await + .is_some(), + "a relative BUNDLE_APP_CONFIG resolves against the root" + ); + } + /// #577: the global config file follows `Settings#global_config_file`: /// `$BUNDLE_CONFIG`, else `$BUNDLE_USER_CONFIG`, else /// `$BUNDLE_USER_HOME/config`, else `~/.bundle/config`; empty values diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs b/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs index 944435d03..8c2a0d793 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs @@ -917,7 +917,10 @@ mod tests { fn bun_lock_remedies_name_the_forced_reinstall() { for file in ["bun.lockb", "bun.lock", "packages/app/bun.lockb"] { let remedy = checkout_remedy(&[file.to_string()]); - assert!(remedy.contains(&format!("`git checkout -- {file}`")), "{remedy}"); + assert!( + remedy.contains(&format!("`git checkout -- {file}`")), + "{remedy}" + ); assert!(remedy.ends_with( ", then run `bun install --force` (a plain `bun install` keeps the patched copy)" ), "{remedy}"); diff --git a/docs/migrating-to-v5.md b/docs/migrating-to-v5.md index 25b12b257..b80e9f2c2 100644 --- a/docs/migrating-to-v5.md +++ b/docs/migrating-to-v5.md @@ -90,7 +90,7 @@ Remove only the Socket-managed portions of old hooks, preserving other commands: | npm / pnpm / yarn / bun | Remove the Socket Patch `apply --silent --ecosystems npm` command from `package.json`'s `postinstall` and `dependencies` scripts; remove empty script keys | | Composer | Remove `socket-patch apply --offline --silent --ecosystems composer` from `post-install-cmd` and `post-update-cmd` | | Python | Remove `socket-patch[hook]` from requirements or project dependencies, and uninstall `socket-patch-hook` in affected environments | -| Bundler | Remove the managed `plugin "socket-patch", path: ...` Gemfile block; run `bundle plugin uninstall socket-patch`; remove `.socket/bundler-plugin/`, `.socket/gem-plugin-stamp`, and its `.socket/.gitignore` entry | +| Bundler | Remove the managed `plugin "socket-patch", path: ...` Gemfile block; remove `.socket/bundler-plugin/`, `.socket/gem-plugin-stamp`, and its `.socket/.gitignore` entry. Then, in **every** checkout that ran `bundle install` under v4 (each developer machine and persistent CI runner, not only yours), run `bundle plugin uninstall socket-patch` or delete `.bundle/plugin/`: the registration lives in the uncommitted `.bundle/`, and once the plugin directory is gone Bundler 2.3–2.5 fail every `bundle install` with a `LoadError` (2.6+ warn on each run). `scan` and `apply` report a leftover registration as `gem_bundler_plugin_stale` | Use `socket-patch list` to inspect the remaining patch set. For agent projects, run `socket-patch apply` once after migration to confirm the manifest still applies. From 687e9fc2d69459e16431dbf5ca865d604851d49a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 9 Oct 2026 19:29:45 +0000 Subject: [PATCH 3/3] Keep unrelated upstream module untouched Assisted-by: Claude Code:claude-opus-5-5 --- crates/socket-patch-core/src/patch/redirect/upstream/mod.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs b/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs index 8c2a0d793..944435d03 100644 --- a/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs +++ b/crates/socket-patch-core/src/patch/redirect/upstream/mod.rs @@ -917,10 +917,7 @@ mod tests { fn bun_lock_remedies_name_the_forced_reinstall() { for file in ["bun.lockb", "bun.lock", "packages/app/bun.lockb"] { let remedy = checkout_remedy(&[file.to_string()]); - assert!( - remedy.contains(&format!("`git checkout -- {file}`")), - "{remedy}" - ); + assert!(remedy.contains(&format!("`git checkout -- {file}`")), "{remedy}"); assert!(remedy.ends_with( ", then run `bun install --force` (a plain `bun install` keeps the patched copy)" ), "{remedy}");