Repository navigation
Write shared Go patch replaces into go.work - #1764
Merged
Mikola Lysenko (mikolalysenko) merged 47 commits intoOct 28, 2026
Merged
Conversation
When nothing imports a package from a hosted-patched Go module (it is only a transitive requirement), `go mod tidy` keeps just the socket module's `/go.mod` go.sum line and drops its zip line. The build still succeeds offline, but VEX discovery required both lines, so the patch was skipped as `package_not_found` instead of attested. The `/go.mod` line for the exact socket module+version is now enough to recognise the pin. Without the zip line the ref has no locked integrity and is not integrity-required, so it attests from the lockfile: go compiles none of the module's packages, and the module path carries the patch uuid. A zip line, when present, must still be the single well-formed hash and supplies `locked_integrity`. Conflicting hashes or a malformed line for the socket module+version still mean no pin. The hosted writer is unchanged: it still writes both lines and refuses a module outside the build graph. Assisted-by: Claude Code:claude-opus-5-5
A `/go.mod`-only go.sum line also exists for versions MVS reads but does not select, so in a go < 1.17 graph a `/go.mod`-only hosted pin whose version go.sum shows is not selected is now reported as inert instead of attested. In workspace mode go verifies against go.work.sum and every member's go.sum, so discovery now reads those too (in-root members only) and a member's zip line still supplies the locked integrity. go.sum parsing now follows go: a line of other than three tokens makes the whole file unusable, a non-dirhash `h1:` breaks that module's pin, and hashes of other algorithms are ignored. Re-running the hosted rewrite over a project `go mod tidy` pruned to the `/go.mod` line now leaves go.sum alone instead of re-adding the zip line the next tidy removes again. Assisted-by: Claude Code:claude-opus-5-5
Assisted-by: Claude Code:claude-opus-5-5
In a go.work workspace, Go builds the highest version any `use` member requires. A Socket replace whose go.sum held only the `/go.mod` line was still attested when another member required a newer version, so VEX could claim a patch that no build links. Such a pin now attests only when every member's go.mod is inside the project and readable and together they select the replaced version; otherwise it is flagged as inert or left unattested. Pins are also looked up only in the sum files Go actually reads: in a workspace, go.work.sum and the members' go.sum (the root go.sum only when `.` is a member); with GOWORK=off, the root go.sum alone, and the go.work replaces are ignored as Go ignores them. Assisted-by: Claude Code:claude-opus-5-5
`go mod tidy` drops the Socket module's zip checksum line while no package of the patched module is imported. If the project later imports it, the build fails with "missing go.sum entry", and re-running `scan --mode hosted` left that state untouched, so the project had no supported way back. A re-run now always writes both go.sum lines, so it repairs the project and the build uses the patched module again. The trade-off is that the next `go mod tidy` may drop the line again while the module stays unimported. Assisted-by: Claude Code:claude-opus-5-5
A go.work build never makes a member's go.mod list the versions it builds, and it adds missing checksums to go.work.sum on its own. So a member's dependency can pull in a newer version of a patched module than any committed file names, and the build links the unpatched module. vex used to attest such a pin from the lockfile whenever the members' go.mod files named the patched version. In a workspace a pin with only the /go.mod line now needs a verifying installed copy, and a newer version in a member's require or in any recorded zip line marks the replace as inert. Docs and the CLI contract say so, and a real-go test covers the member dependency case. Assisted-by: Claude Code:claude-opus-5-5
go only ever appends to go.work.sum: once a member stops depending on whatever raised a module, the zip line of the higher version stays behind. vex treated such a line as proof the workspace builds that version, so it dropped a Go /go.mod-only pin as inert even when go builds the patched version. Only a member's require or the zip lines of a member's go.sum now mark a workspace pin inert. The pin still needs a verifying installed copy to attest. Assisted-by: Claude Code:claude-opus-5-5
A workspace's sum files are only appended to, so the socket zip line a past build wrote to go.work.sum (or a member go.sum) stays after a member starts requiring a higher version of the replaced module. Go then builds that higher version and the replace is inert, but discovery attested the zip-pinned ref from the lockfile anyway, and the CLI's post-gate reads only the root go.mod and go.sum, so vex claimed a patch the build never links. Run the same inert check GoModOnly pins already get on Zip pins in workspace mode. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
force-pushed
the
feat/golang-workspace-writer
branch
from
October 28, 2026 04:22
4693baa to
021f88a
Compare
Each Go command checked go.work its own way, and some of those checks disagreed with go. A replace pinned to v1.0.0 counted as patched even when another workspace member required v1.1.0, so go built the newer, unpatched version while apply, vendor and vex treated it as fixed. A go.work replace of a different version was taken to override a member's replace, though go still uses the member's. GOWORK=auto was treated as a file name, and a relative GOWORK, which go rejects, was read as if it were in effect. Every Go path now asks one workspace model. It finds go.work as go does, picks the version the whole workspace builds from all members' requirements, and applies go's replace precedence. A member go.mod that can't be read leaves the answer unknown, so nothing is attested on a guess. VEX now also reads member go.mod files from a go.work root that has no go.mod of its own, and apply and vendor refuse there and name the members to run them in. Assisted-by: Claude Code:claude-opus-5-5
`scan --mode hosted` wrote a Go replace without looking at the go.work in effect, and only warned afterwards. A replace that conflicted with another member's broke every build in the workspace, and one that go.work overrode was reported as redirected while go built something else. Hosted mode now checks the workspace before writing. A replace go.work would override, ignore or reject is not written, the run warns `redirect_golang_workspace_conflict`, and the patch is not reported as redirected. The version check uses the whole workspace too, so a member that doesn't require the module can still carry the replace when another member does, and one that raises it to a newer version refuses the pin. From a go.work root with no go.mod of its own, the pin goes into each member that lists the module. Assisted-by: Claude Code:claude-opus-5-5
`rollback` only looked at the root go.mod and go.sum. A hosted pin in go.work was refused with a "restore it from version control" error, the Socket module's lines that go writes to go.work.sum in workspace mode were left behind, and a pin in a workspace member's go.mod was not found from the workspace root. The restore now edits the go.mod or go.work each pin was found in, removes the Socket module's lines from that file's go.sum, go.work.sum and, for a go.work pin, the members' go.sum, and puts the upstream module's lines back where the Socket ones were. Assisted-by: Claude Code:claude-opus-5-5
Real-go tests now cover a go.work root without a go.mod (hosted pins in each member, attested from the root, rolled back byte for byte), a member raising the patched module's version, GOWORK unset, `auto`, `off` and relative, the hosted go.work gate under go's replace precedence, and rollback of a go.work pin and its go.work.sum lines. Each test fails before this branch's fixes. The CLI contract and the Go ecosystem docs now describe how the workspace is found, how its version and replace precedence are read, and what each command does from a go.work root. Assisted-by: Claude Code:claude-opus-5-5
In a go.work workspace go keeps no member's go.mod tidy, so a member's dependency can raise a patched module above the version the Socket replace is pinned to; the only trace is the zip line go writes to go.work.sum or a member's go.sum. A higher zip line now proves the pin inert even when a member's go.mod is out of reach, for vex and for apply, vendor and hosted writes. Another member replacing every version of the module (replace M => ...) now blocks apply, vendor and hosted writes and makes vex leave the patch out: go links that target or refuses to build. At a go.work root, a hosted pin lands in every member that lists the module or in none, instead of staying half-written when one member refuses. When GOWORK names a file other than the project's go.work, or a relative path go rejects, vex attests no Go patch and reports each one. Assisted-by: Claude Code:claude-opus-5-5
In a go.work workspace three things made socket-patch report a Go patch as applied, or refuse one, when go did otherwise: - A module that is itself a `use` member (a local checkout) is built from its directory, and go ignores every replace of it. apply, vex and hosted mode still wrote and attested the replace. - go only ever appends to go.work.sum, so a zip line of a version that nothing requires any more stayed behind and made apply and hosted mode refuse a pin that go builds. - A member's dependency can raise the module past every member require. Before a build wrote go.work.sum nothing showed it, so a fresh clone attested a pin that go ignores. The selection is now worked out the way go does it: minimal version selection over each member's requires and the go.mod of every dependency go loads, read from its replacement directory or the module cache. Sum files are no longer evidence. When a needed go.mod is missing the answer is unknown: apply --check fails, vex leaves the patch out (go_version_not_selected) and hosted mode writes nothing, until `go mod download` fills the cache. A replace of a workspace module is refused as a workspace conflict. Assisted-by: Claude Code:claude-opus-5-5
The repo guard forbids private --json envelope readers in test files, so the workspace e2e reads warning codes through the shared helper. Assisted-by: Claude Code:claude-opus-5-5
With the workspace model, the go.work-level check reports a raised zip-pinned replace as inert (the workspace requires the higher version), so the test asserts the verdict and version, not the module-mode sentence. Assisted-by: Claude Code:claude-opus-5-5
Run in the directory holding go.work, apply, vendor and hosted scans now replace a module the workspace shares once in go.work, the way `go work edit -replace` does: when that directory has no go.mod of its own, when another member lists the module, or when go.work already holds our replace of it. Before, member replaces of one patched copy resolved to different directories and go refused the workspace, so the second member was refused and the docs told users to move the replace by hand; a go.work root without a go.mod could not be patched by apply or vendor at all. The vendored copy stays at the root and the go.work target is relative to it. A hosted pin is version-specific and its sum lines go into go.work.sum, leaving the members' go.mod and go.sum untouched. A go.work replace is refused when go.work already replaces that version with something else, or a member's own replace would be overridden. The vendored ledger records go.work as the wiring file, and the project's replaces are read from both files, so --check, remove, vendor --revert, list and vex find and restore them there. Rollback of a go.work pin no longer adds upstream sum lines that were never removed, and deletes a go.work.sum the pin alone created, so go.work and go.work.sum come back byte for byte. Assisted-by: Claude Code:claude-opus-5-5
Describe when each Go mode writes its replace into go.work, how the target and go.work.sum pin look, what a GOWORK=off build sees, and how check, revert, vex and rollback treat it. Drop the advice to move the replace into go.work by hand. Assisted-by: Claude Code:claude-opus-5-5
Real-go e2e from a go.work root with no go.mod, two members requiring the same module: vendor, apply and hosted each write one replace, both members build the patched module with -mod=readonly and go list -m all shows the replacement, a re-run changes nothing, and vendor --revert and rollback give go.work back byte for byte. Assisted-by: Claude Code:claude-opus-5-5
A hosted scan that pins a module in go.work now checks a committed workspace vendor/modules.txt too, and names `go work vendor` when it does not record the new replace, as it already did for go.mod pins. Assisted-by: Claude Code:claude-opus-5-5
A replace that apply wrote into the root go.mod, before another workspace member required the module, now moves into go.work the next time apply or vendor writes it. Before, vendor added the go.work line, left the go.mod line behind and then deleted the copy it named, so a GOWORK=off build of the root failed and vendor --revert kept the dangling line. The ledger now records that original against go.mod. Rolling back hosted pins of one patch held in several members' go.mod and in go.work now puts the upstream go.sum lines back in every member's go.sum and deletes the go.work.sum the pin created. Before, only the first member got its lines back, so the others failed -mod=readonly builds, and go.work.sum was left holding upstream lines. apply, vendor and hosted mode again refuse a project whose go.mod the go.work beside it does not use, instead of writing a go.work replace that patches the other members but never the project itself. Assisted-by: Claude Code:claude-opus-5-5
Run at a go.work root without a go.mod of its own, apply, vendor and hosted mode write a shared replace into go.work. When the replaced module is itself a `use` member (a local checkout), go builds it from that directory and ignores the replace, so the patch was reported as applied while go linked the local, unpatched copy. The go.work check now refuses that replace, as the member go.mod check already does. The e2e test runs hosted mode from the root as well, and the shared-module re-run test fills the module cache first, since a workspace's selection is read from it. Assisted-by: Claude Code:claude-opus-5-5
apply writes a shared replace into go.work, and apply, remove and rollback rewrite go.mod, with an atomic rename. When either file was a symbolic link the rename replaced the link with a regular file, so the user's link was gone for good, while vendor and hosted mode refused the same layout and wrote nothing. apply now refuses before it copies anything when the file that would hold the replace is a link, and every go.mod or go.work edit refuses to change a symlinked file, so remove and rollback leave it alone too. Assisted-by: Claude Code:claude-opus-5-5
A replace appended to a go.mod or go.work that had no final newline ended with one, and remove, vendor --revert and rollback had no way to tell it was added. The file then kept an extra newline after a full revert, so the tree showed a diff the docs say it won't. The appended replace now ends without a newline when the file did, and removing it restores the file byte for byte. The hosted rewriter's golden digests change for the seeded go.mod files without one. Assisted-by: Claude Code:claude-opus-5-5
The docs said rollback deletes a go.work.sum "the pin alone created". Hosted mode keeps no record of which files existed before a pin, so it deletes any go.work.sum that stripping the Socket lines leaves empty, including one a user created empty. go never writes an empty go.work.sum, so that only happens to a file made empty by hand. Say so, rather than claim rollback can tell the cases apart. Assisted-by: Claude Code:claude-opus-5-5
vex reads which version a go.work workspace selects from its module graph in the module cache, and since its go.work replaces are checked from the workspace root, the go.work flavor of the lockfile fixtures left them out with go_version_not_selected. The fixture now puts the patched module's go.mod in its private cache, as `go mod download` would. Assisted-by: Claude Code:claude-opus-5-5
When the replace host is go.work and the root module mirrors it in go.mod, apply's in-sync fast path and `apply --check` accepted any matching Socket replace, so the go.work line alone counted. A deleted go.mod mirror then made apply a no-op and `--check` pass while a GOWORK=off build linked the unpatched module (Bugbot on golang_local.rs:910). Both now require the replace in every file apply writes it into (the host plus the mirrored root go.mod), via superseded_replace. Co-Authored-By: Claude <noreply@anthropic.com>
Collaborator
Author
|
bugbot run Generated by Claude Code |
Main landed lighter fixes for the same go.work gaps this PR models in full: #1752 (version gate reads every member), #1753 (hosted pins outranked by another member), #1767 (stale go.work vendor warning) and the squashed #1748. Resolve them onto this PR's GoWorkspace model instead of keeping two workspace readers: - go_build_list: keep project_selection on GoWorkspace. Drop main's ModuleEvidence / workspace_selection / other_workspace_members and its three on-disk selection tests: GoWorkspace::selection covers those cases from the module graph (go_workspace tests), and without a module cache it answers Unknown by design. - hosted: drop #1753's go_members plumbing (engine RewriteOptions closure, rewrite_golang argument, CLI wiring). The PR's HostedGoWorkspace already refuses a pin another member outranks. Main's e2e test for it now seeds the module cache the workspace graph reads, and passes, including the rescan that removes the pin. - go_consumer_sync: port #1767's modules.txt audit (per-member replace resolution, member_owned, unreadable members skipped) onto go_workspace::load and its shared same_dir_key / lexical_target. - vex discover, CLI_CONTRACT, e2e_golang_hosted_build: keep the PR's stricter rules (a zip pin is checked against the workspace selection too; sum-file zip lines are no evidence of it). Co-Authored-By: Claude <noreply@anthropic.com>
673e246 made the in-sync check also require the root go.mod mirror of a go.work replace. That was too strict and too costly: a replace held only in the root go.mod still patches the workspace (the e2e positive control in go_work_without_the_project_ignores_its_replace went red), and a missing mirror sent apply down the rebuild path, whose failure teardown drops the go.work replace the workspace builds through (Bugbot on golang_local.rs:713). Now only the mirror itself is checked: `--check` reports it missing, and apply's hot path writes it back with ensure_replace_entry, leaving the copy and the go.work replace alone. Co-Authored-By: Claude <noreply@anthropic.com>
Collaborator
Author
|
bugbot run Generated by Claude Code |
… feat/golang-workspace-writer
Brings in the base's main merge (678f90e). It replaced go_consumer_sync's same_replacement with lexical_target comparison (main #1767), so the go.work modules.txt check here now compares the go.work replace and the recorded line the same way, both resolved against the go.work directory. Co-Authored-By: Claude <noreply@anthropic.com>
Collaborator
Author
|
bugbot run 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 6249c81. Configure here.
Collaborator
Author
|
Ready for review at
Generated by Claude Code |
Tanmay Singla (Tanmay182003)
approved these changes
Oct 28, 2026
Main squash-merged #1763 (the workspace-model branch this PR had been merging in), so every model file conflicted. No main commit since the squash touches these files; the only model commit this branch lacked was ed7d030 (keep a workspace pin of a dependency-raised version live), which this merge brings in via vex/discover/golang.rs. Resolved with the last model tip this branch merged (678f90e) as the base, keeping the writer's go.work changes intact. Co-Authored-By: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
deleted the
feat/golang-workspace-writer
branch
October 28, 2026 21:56
Mikola Lysenko (mikolalysenko)
pushed a commit
that referenced
this pull request
Oct 28, 2026
#1764, which this PR stacked on, squash-merged into main as eb75d20, so every conflict was between this branch's older copy of that stack and the landed version. Take main's side for all of them and keep this PR's only unique change: the two go.work tests in e2e_golang_workspace_build.rs (go work vendor, and which file a run location writes the replace to). The full e2e_golang_workspace_build target passes locally (30/30). Co-Authored-By: Claude <noreply@anthropic.com>
Mikola Lysenko (mikolalysenko)
pushed a commit
that referenced
this pull request
Oct 28, 2026
Resolve conflicts with main's go.work replace hosting (#1763, #1764, #1765). ensure_replace_entry now validates and backs up go.sum before writing the replace into main's replace host (go.work or go.mod), and drop_replace_entry restores the saved go.sum lines before removing the replace from go.work and go.mod. The cleanup fallback uses the same go.work-aware removal now that edit_go_mod is gone. The workspace e2e test keeps this branch's reinstall steps on main's tail(&Tail{..}) shape. Co-Authored-By: Claude <noreply@anthropic.com>
This was referenced Oct 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on the workspace-model PR.
Problem
No mode ever wrote a Socket replace into go.work. Agent and vendored replaces are directory paths relative to each member, so two members that use the same patched module always got different targets. Go refuses that with "conflicting replacements", so the second member was refused, and the docs told users to move the replace into go.work by hand. Patching a module shared by several workspace members therefore didn't work.
Change
go work edit -replacesemantics):--check, VEX and list discover it.vendortaking over anapplyredirect in a root go.mod moves it into go.work and drops the old line.usethe project still gets the "not a member" refusal, with advice to rungo work use ..vendor/is stale.Validation
All runs used real go1.26.3:
New real-go tests cover:
go build -mod=readonlyandgo list -m all;tests/equivalence/golang_rewrite.goldenwas re-blessed for the newline change.🤖 Generated with Claude Code
Note
Medium Risk
Changes how Go patch wiring is written and rolled back across workspace layouts, affecting apply, vendor, hosted redirect, VEX, and rollback; mistakes could leave members unpatched or break
GOWORK=offbuilds.Overview
Shared Go workspace patches now land in
go.work(and hosted checksums ingo.work.sum) when the workspace build uses the module, instead of duplicating per-membergo.modreplaces that Go rejects as conflicting. Apply, vendor, and hosted scan use this path; a root module that is ausemember still gets a mirror replace in rootgo.modsoGOWORK=offbuilds stay patched. Vendor can move an existing rootgo.modredirect intogo.workand repoint both files at the vendored copy.Rollback / upstream restore is updated for go.work pins (including removing an empty
go.work.sum), and VEX treats Socket’s own go.work replace as attestable rather than a workspace override. Discovery, hosted candidate files, consumer sync (go work vendor/ stalevendor/), and symlink refusal ongo.work/go.modare wired through. CLI_CONTRACT.md and extensive e2e tests reflect the new contract.Reviewed by Cursor Bugbot for commit 6249c81. Configure here.
Generated by Claude Code