Repository navigation
Fix Go replace written to unused go.work (#1810) - #1812
Conversation
Assisted-by: Claude Code:claude-opus-5-5
With GOWORK naming another file beside the project's go.work (say GOWORK=$PWD/ci.work), apply put the shared replace into ./go.work, which go does not read then, and reported the patch as applied while every member built the unpatched module. With no ./go.work it failed with a misleading "read ./go.work" error. The workspace now counts as the project's own only when the file in effect is <root>/go.work, the same rule vex discovery uses. A sibling workspace file is handled like one in another directory: a root module it uses takes the replace in its go.mod (which go applies), and a root with no module is refused with "has nowhere to go", writing nothing. The hosted planner shares the check. Fixes #1810 Assisted-by: Claude Code:claude-opus-5-5
3e6b9c6 to
267177b
Compare
A root with no go.mod is now refused before the "already replaced the same way" shortcut, so a sibling GOWORK file that already holds our replace no longer ends in a raw "read go.mod" error. When the root has its own go.work that GOWORK passes over, the refusal says so and how to fix it instead of claiming the directory has no go.work. The rooted check compares the file name and directory without resolving a symlinked go.work, so a root go.work linking elsewhere keeps its old handling. Refs #1810 Assisted-by: Claude Code:claude-opus-5-5
|
BugBot review Generated by Claude Code |
There was a problem hiding this comment.
✅ Bugbot reviewed your changes and found no new issues!
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit f371126. Configure here.
|
Labeled Ready for review by the burn-down agent.
Generated by Claude Code |
|
[agent] Correction to the reviewer-focus line above: this PR does not write to the file Generated by Claude Code |
|
Burn-down agent: labeled Ready for review at
Slack announcement not sent this run (the Slack send tool was not available to the agent); the next run retries. Generated by Claude Code |
Final review briefWhat it does: When Risk: low. The change tightens one predicate and moves one early return. Every new outcome either writes a Look here
Verified
Changes I made: none. Open questions: none blocking. Possible follow-ups:
Auto-merge 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 #1810
Summary
Suppose
GOWORKnames another workspace file in the project directory, for exampleGOWORK=$PWD/ci.worknext to a developergo.work. Before this change,applywrote the shared module'sreplaceinto./go.work, which go does not read in that case. It then reported1 of 1 targeted patch appliedand exited 0, while every member still built the unpatched module. If there was no./go.workat all, it failed with a misleadingread ./go.work: No such file or directory.After this change socket-patch never writes the replace into a
go.workthat go is not reading:ci.workuses: the rootgo.modgets the replace, and go applies it workspace-wide. Both members build the patched copy.apply/vendorrefuse and write nothing. This is the same as v5.0.1 and the same as a workspace file in another directory. The message names the cause:GOWORK selects ci.work, so go doesn't read this directory's go.work … unset GOWORK (or point it at this go.work), or run socket-patch in a member (a, b).Root cause
GoWorkspace::rooted_at(vendor/go_workspace.rs) decided that "the workspace sits at the project root" by comparing only the directory of the workspace file in effect.ReplaceHost::GoWorkalways means the file<root>/go.work. So a siblingci.workmadereplace_hostpickGoWork, and every writer (ensure_replace_entry, the vendored backend,go_consumer_sync::replace_refusal, and the hosted planner inpatch/redirect/mod.rs) targeted<root>/go.work.The fix makes
rooted_atrequire that the file in effect is<root>/go.work: file namego.workand same directory. VEX discovery already uses this rule (unread_workspace), so writers and VEX now agree. Every caller goes throughrooted_at/replace_host, so apply, vendor and hosted are all covered by this one check./code-review highfound three more problems, fixed in f371126:GoWorkspace::conflictnow returnsNoModulefor a root with no go.mod before its "the workspace already replaces it the same way" early return. Otherwise a siblingci.workthat already held our replace would fall through to a rawread go.moderror.NoModulenow carriesown_work, so the message no longer claims the directory "holds neither go.work …" when GOWORK passes over its go.work.rooted_atcompares the file name plus the directory key, not the canonicalized file path. A rootgo.workthat is a symlink to a file elsewhere therefore keeps its previous handling.Not changed (code-review findings answered)
ci.workinstead of refusing (carry the path inReplaceHost::GoWork). The issue allows either behavior. Writing into an arbitrary GOWORK file would mean rollback, remove, vendor revert, the vendored ledger priors and the hosted state all have to find that file again later, even afterGOWORKchanges or is unset. That is a much larger change. This PR restores the v5.0.1 fail-closed behavior and keeps the working case (root module member). Supporting writes to a GOWORK file can follow as an enhancement if wanted.root_work_filestill matches by directory. It is used by readers (read_replace_entries,audit_work, the vex root list, the go crawler's "is a Go project" check, the hosted-takeover message). Those correctly describe what go reads, includingci.work's replaces. Narrowing it would make the crawler treat a ci.work-only root as "not a Go project", soapplywould silently do nothing instead of refusing. The doc comment now spells out the reader/writer split.redirect_golang_no_go_modwarning (skipped, nothing pinned). It fails closed and is not attested, but the wording is generic. That is a cosmetic follow-up.Test evidence
Red → green for #1810 (the tests were written first; the fix was then reverted locally to show red):
go_workspace::tests::a_sibling_gowork_file_is_not_the_roots_go_work(unit)assertion failed: !ws.rooted_at(root)e2e_golang_workspace_build::apply_never_writes_go_work_when_gowork_names_a_sibling_file(go 1.24.7: no-module root with and withoutgo.work, then a root module member; members built withGOWORK=ci.work)applyexit 0,"applied":1A: PATCHED,B: PATCHED; go.work and ci.work byte-identical)Commands run locally (Linux, go 1.24.7):
cargo fmt --all -- --check: okcargo clippy --workspace --all-features -- -D warnings: okcargo test -p socket-patch-cli --all-features --lib --bins: 936 passedcargo test -p socket-patch-core --all-features --lib: 6924 passed, 4 failed. The 4 failures are read-only-permission tests (copy_tree::relax_loop_must_not_traverse_symlinked_root,vlt_heal::an_unremovable_hidden_lock_keeps_every_store_entry,pypi_poetry::wire_write_failure_…,pypi_requirements::wire_failure_rolls_back_…). They cannot fail a write when run as root (uid 0 in this container) and do not touch Go code.e2e_golang_workspace_build(31),e2e_golang_build,e2e_vendor_golang_build,e2e_golang_hosted_build,e2e_golang,e2e_golang_hosted_state: all pass (182 tests).cargo test --workspacebuild ran out of container disk (31 GB target dir), so CI is the source of truth for the rest.Per-issue checklist
./go.work(unit + e2e case 1); no misleadingread ./go.workerror without it (e2e case 2); a root module member is patched through go.mod (e2e case 3); docs/ecosystems.md describes the rule.https://claude.ai/code/session_011f9aePTExGy6yC7ztexCYe
Note
Medium Risk
Changes where Go module replaces are written and when apply refuses; incorrect logic could still mis-route patches, but the change is fail-closed and covered by new tests.
Overview
Fixes #1810: when
GOWORKpoints at a sibling workspace file (e.g.ci.work) beside./go.work,apply/vendor/hostedno longer treat the root as ago.workreplace host and write into./go.work, which Go does not read in that setup.GoWorkspace::rooted_atnow requires the active workspace file to be literallygo.workin the project root (name + directory), not merely any workspace file whose directory matches the root. Writers that pickReplaceHost::GoWorktherefore skip the unread./go.work.Conflict handling returns
NoModuleearly when the root has nogo.mod(even if the selected workspace already has the same replace), avoiding a misleadingread go.moderror.NoModuleaddsown_workso refusal text explainsGOWORKbypassing a localgo.work.Root module in the workspace still gets the replace in
go.mod; module-less roots fail closed with no file changes. Docs and new unit/e2e tests cover the three layouts.Reviewed by Cursor Bugbot for commit f371126. Configure here.
Generated by Claude Code