Skip to content

Go apply writes the shared replace into ./go.work when GOWORK names another file in the same directory, reports success, and the workspace builds unpatched #1810

Closed

Description

[agent] Found by the scheduled Go modules bug-hunt routine (ledger #317).

Summary

Since #1764 (eb75d20), apply run at a go.work root puts the shared module's replace into go.work. It decides that from the workspace go actually uses (GOWORK), but it always writes the file <root>/go.work. When GOWORK names another file in the same directory (for example GOWORK=$PWD/ci.work next to a developer go.work), the replace lands in a file go doesn't read. apply still reports 1 of 1 targeted patch applied and exits 0, re-runs do the same, and every member builds the unpatched module.

apply --check (exit 1, "missing go.mod replace") and vex (not_applied, exit 1) both catch it, so nothing gets attested. But apply's own success report is wrong, and it changes a file the user didn't ask it to change.

If <root>/go.work doesn't exist at all (only ci.work), apply fails with a misleading failed to update go.mod: read ./go.work: No such file or directory.

v5.0.1 and the parent of eb75d20 (86457da) fail closed on the same fixture: apply exits 1 and writes nothing.

Impact

CI setups that pick a workspace file with GOWORK=/abs/path/x.work (a documented go feature, and docs/ecosystems.md says socket-patch honours it) get a green apply and an unpatched build. The stray replace also changes the developer go.work.

Repro (Linux, go 1.24.7, hermetic file GOPROXY, hand-staged .socket/manifest.json and blob as in e2e_golang_workspace_build.rs::write_local_patch)

# ws/ has no go.mod; members ./a and ./b both `require example.com/upstream v1.0.0`
# (lib returns "PRISTINE"; the patch blob makes it return "PATCHED")
cd ws
printf 'go 1.18\n\nuse (\n\t./a\n\t./b\n)\n' > ci.work   # the workspace CI uses
printf 'go 1.18\n\nuse ./a\n' > go.work                  # a developer's go.work
export GOWORK=$PWD/ci.work
socket-patch apply --offline --ecosystems golang
#  Summary: 1 of 1 targeted patch applied ...   exit 0
grep -l replace go.work ci.work a/go.mod b/go.mod
#  go.work                                       <- only the file go isn't reading
(cd a && go run .); (cd b && go run .)
#  A: PRISTINE
#  B: PRISTINE
socket-patch apply --check --ecosystems golang   # exit 1: missing go.mod `replace`
socket-patch apply --offline --ecosystems golang # exit 0 again, still unpatched

Controls: with GOWORK unset, the same go.work replace makes a print PATCHED (which shows go really reads ci.work above). With a root go.mod that is a use member, the mirrored root go.mod replace happens to patch the build, but apply still adds the replace to the unused go.work.

Expected vs actual

  • docs/ecosystems.md, "Go workspaces (go.work)": "The go.work in effect is GOWORK when it names an absolute file … Run in the directory holding go.work, apply, vendor and scan --mode hosted write the replace … into go.work." Expected: the replace goes into the file in effect (ci.work), or apply refuses. Actual: it goes into ./go.work, and apply reports the patch as applied.
  • CLI_CONTRACT: apply exits 0 only when the patches it reports as applied take effect.

OS × version

OS go main ecd4924 v5.0.1 / 86457da
Linux 1.21.13 fail (exit 0, PRISTINE) —
Linux 1.24.7 fail (3 fresh fixtures) refuses (exit 1, nothing written)
Linux 1.26.8 fail —
macOS / Windows — untested (no probe branch this run) —

Untested: vendor and scan --mode hosted (the hosted engine also builds go.work paths itself, see hosted/engine.rs:554), and rollback/remove of a replace sitting in a non-go.work workspace file.

First bad commit

eb75d20 "Write shared Go patch replaces into go.work (#1764)". Its parent 86457da refuses.

Suspect code

  • crates/socket-patch-core/src/vendor/go_mod_edit.rs:209: let host_path = project_root.join(host.file_name()); gives <root>/go.work. The host decision comes from the resolved workspace file (GoWorkspace::rooted_at, vendor/go_workspace.rs:642, which compares only its directory).
  • crates/socket-patch-core/src/vendor/go_workspace.rs:238: ReplaceHost::GoWork => "go.work", also used by go_consumer_sync.rs:416 and patch/redirect/mod.rs:9464.
  • crates/socket-patch-core/src/vendor/go_mod_edit.rs:273: drop_replace_entry hardcodes project_root.join("go.work") too.

The host should carry the resolved work_file path, or rooted_at should require the file to be <root>/go.work.

Activity

  1. mikolalysenko commented on Oct 29, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Triaged as priority:p2 (Go modules). Confirmed on main ecd4924 by reading the code: GoWorkspace::rooted_at (vendor/go_workspace.rs) compares only the directory of the go.work in effect, so a GOWORK=$PWD/ci.work counts as "rooted here", and then ensure_replace_entry (vendor/go_mod_edit.rs) writes to project_root.join(host.file_name()), i.e. <root>/go.work. drop_replace_entry hardcodes <root>/go.work as well. Not a duplicate: #1799 / PR #1800 is about GOFLAGS=-modfile choosing the go.mod, a different file-selection path. No open PR fixes this one yet. Open for a fix.


    Generated by Claude Code

  2. mikolalysenko commented on Oct 29, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Claiming this issue (shared root cause: the Go replace host records only go.work vs go.mod, so writers rebuild <root>/go.work instead of the workspace file GOWORK names). Branch: agent/fix-go-gowork-file-path. Claim-ID: 2026-10-29T05:21:28Z-cdeea2


    Generated by Claude Code

  3. mikolalysenko commented on Oct 29, 2026

    @mikolalysenko
    CollaboratorAuthor

    [agent] Draft fix PR: #1812


    Generated by Claude Code

  4. added 3 commits that reference this issue on Oct 29, 2026
    3e6b9c6
    267177b
    f371126
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent:triagedbugSomething isn't workingbughuntFound by a scheduled package-manager bug-hunt agentpm:goGo modulespriority:p2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions