Repository navigation
Fix Go replace-target patches reported applied (#1823) - #1824
Conversation
Assisted-by: Claude Code:claude-opus-5-5
Add an end-to-end regression for #1823: when go.mod replaces a fork with the patched module (`replace other.com/fork => upstream v1.0.0`), go records the upstream's go.sum lines but never applies a replace of the upstream itself. apply, apply --check and vex all reported the patch as in effect while go linked the unpatched code. Assisted-by: Claude Code:claude-opus-5-5
A module the build uses only as the target of another module's replace (`replace other.com/fork => example.com/upstream v1.0.0`) has its go.sum zip line under its own path, so the build-list check took it as built. go doesn't chain replaces, so a Socket replace of it is silently ignored: apply reported success, apply --check passed and vex attested not_affected while the build stayed unpatched. The shared build-list check now ignores a zip line that a module-to-module replace in go.mod explains (go refuses a build that uses one module@version under two paths, so it can't be the module's own) and reports the module as a replace target. apply and vendor refuse it, apply --check flags it, vex leaves it out, the hosted rewriter refuses it (redirect_golang_not_in_module_graph) and VEX discovery treats an existing hosted pin of it as inert. Fixes #1823 Assisted-by: Claude Code:claude-opus-5-5
go mod tidy keeps unused replace directives. A fork replace of a module go.mod doesn't require, one pinned to a version other than the required one, or a version-less one that a versioned replace of the same module shadows doesn't change what go builds, so it must not discount the patched module's own go.sum line. Also stop quoting a version-less directive in the refusal messages, which misquoted a versioned replace, and cover the agent-mode drift and refusal with a unit test. Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
|
Burn-down agent: labeled Ready for review at
Generated by Claude Code |
Final review briefWhat it does. When go.mod swaps a dependency for a fork with Risk: low–medium. Changes the shared Go selection logic used by apply, vendor, scan, vex and VEX discovery, but only for the narrow case of an exact-version replace target, and the new answer is always the safer one (refuse/flag rather than claim patched). Look here
Verified
Changes I made: none. Open questions (non-blocking)
Auto-merge (squash) is armed, so approving sends this straight to the merge queue. Generated by Claude Code |
LLM Description written by Claude Code:claude-opus-5-5
Fixes #1823
Summary
Go projects often swap a dependency for a fork with a replace like
replace other.com/fork => example.com/upstream v1.0.0. When a patch targetsexample.com/upstream@v1.0.0in that setup, socket-patch reported it applied (applyexit 0,apply --check"in sync",vexnot_affected), butgo buildstill linked the unpatched code. This PR makes every Go path refuse the patch or flag it instead.Root cause
go_build_list::selecttreats a go.sum zip line formodule@versionas proof that the build compiles that module. go records a module-to-module replace target's go.sum lines under the target's own path, but the build list holds only the replaced module (other.com/fork). go doesn't chain replaces, so a Socket replace ofexample.com/upstreamdoes nothing.selectstill answeredSelectedfor go ≥ 1.17, and for go < 1.17 it read the zip as the built version. All of these callers share that answer: agentapply/apply --check,vendor,scan --mode hosted, CLIvexand VEX lockfile discovery.Fix
selectnow takes the root go.mod text. It ignores a zip line at the version that an in-effect module-to-module replace targets, and reports a newSelection::ReplaceTarget(via). A replace counts as in effect when go.mod requires the replaced module, any LHS version matches the required version, and no versioned replace shadows a version-less one.go mod tidykeeps unused replaces, so stale ones are skipped. Ignoring the zip is sound because go refuses to build when one module@version is used under two module paths, so that zip line can't come from the target being built in its own right.applyandvendorrefuse the patch (selection_refusal)apply --checkreports a newDrift::ReplaceTargetOnlyvexleaves the patch out (go_version_not_selected)redirect_golang_not_in_module_graphand removes a stale hosted pin it wrote earlierdocs/ecosystems.md(Go section) explains the new refusal.Tests (red → green)
applyrefuses,apply --checkexits 1,vexomits; versioned and version-less fork replace; go 1.21 and go 1.16e2e_golang_build::module_built_only_as_a_replace_target_is_not_patched(real go toolchain, hermetic file GOPROXY)go_build_list::tests::replace_target_zip_is_not_the_module_built,replace_target_at_another_version_keeps_its_own_evidence,inactive_replace_target_keeps_the_zip_as_evidencegolang_local::tests::test_replace_target_only_module_is_refused_and_flaggedredirect::tests::golang_replace_target_only_module_skipsvex::discover::golang::tests::pin_of_a_module_built_only_as_a_replace_target_is_inertRed: with the
src/changes stashed, the e2e fails atapply --check must flag the inert replace: left: 0 right: 1. Green: it passes with the fix (go 1.24.7).Local:
cargo fmt --all -- --check✅,cargo clippy --workspace --all-features -- -D warnings✅,cargo test --workspace --all-features15,253 passed / 11 failed. The 11 failures are environmental and outside the Go code: the sandbox runs as root, which defeats the read-only-permission tests (copy_tree, vlt_heal, pypi wire-failure, repair lock, redirect write-failure); it has no route to patches-api; and the bun lockb fixtures depend on the local setup. CI is the reference for those. Every Go suite passed (e2e_golang_build,e2e_golang_workspace_build,e2e_golang_hosted_*,e2e_vendor_golang_build, core Go unit tests)./code-review high: findings not fixed
proven(the socket module's zip line) for a replace target. Not fixed on purpose. With the in-effect check, an active fork replace means the upstream isn't in the build list, so go never applies the Socket replace. The hosted rewriter writes both sum lines andgo mod tidykeeps them, so a socket-module zip line there is stale and proves nothing.ReplaceTarget.GoWorkspace::selectionwalks the module graph and already reports such a module asNotInGraph, as the issue confirms. An incomplete graph givesUnknown, whichapply --checkandvexalready flag asUnprovenSelection. Out of scope here.vendor_go_version_not_selected,redirect_golang_not_in_module_graph,go_version_not_selected). These reuse each mode's existing code for "the build won't use this replace", so no new contract codes are added.selectgets both the go.mod text and arequiredvalue derived from it; per-call parsing of replaces.requiredstays separate because VEX discovery passes the maximum across workspace files. The extra parse is trivial next to file IO.Known limitation / follow-up
https://claude.ai/code/session_011f9aePVb5rbxqLrbfAWspU
Generated by Claude Code