Repository navigation
Commit eb75d20
Write shared Go patch replaces into go.work (#1764)
* Keep Go hosted pins after go mod tidy drops zip line
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
* Check selection and workspace sums for tidy Go pins
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
* Document tidy Go pin rules in the CLI contract
Assisted-by: Claude Code:claude-opus-5-5
* Don't attest Go pins a workspace never builds
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
* Let a hosted Go re-run restore a pruned zip line
`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
* Don't attest Go /go.mod-only pins in a workspace
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
* Ignore go.work.sum zip lines as selection proof
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
* Check workspace Zip pins for a higher selected version
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>
* Read Go workspaces the way go does
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
* Check go.work before writing hosted Go pins
`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
* Roll back hosted Go pins in go.work and members
`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
* Test and document Go workspace support
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
* Stop attesting Go workspace pins go may not build
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
* Select Go workspace versions from the module graph
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
* Read Go workspace e2e warnings via common::envelope
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
* Accept the workspace inert wording in zip pin test
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
* Write shared Go replaces into go.work
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
* Document go.work replaces for shared modules
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
* Test one go.work replace for a shared module
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
* Warn on stale vendor/ after a hosted go.work pin
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
* Fix Go workspace replace moves and rollbacks
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
* Refuse a go.work replace of a workspace module
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
* Refuse to rewrite a symlinked go.work or go.mod
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
* Keep a missing final newline in go.mod and go.work
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
* Say when hosted rollback removes go.work.sum
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
* Fill the module cache in go.work vex fixtures
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
* Pin go.work files in hosted redirect candidates
The format registry now lists go.work and go.work.sum as hosted Go
files, so the by-value candidate pin includes them.
Assisted-by: Claude Code:claude-opus-5-5
* Gate go.work replaces on the workspace module graph
The VEX selection gate only read Socket replaces from each module
root's go.mod, so a replace written into go.work itself skipped it and
was attested on a fresh clone with no module graph cached, contradicting
golang_workspace_without_its_module_graph_is_not_attested. Feed the
workspace's own replaces through the same gate (Bugbot, vex.rs:1006).
The go.work fixture now caches the module graph, as `go mod download`
would, since two existing go.work tests relied on attesting with an
empty module cache.
Co-Authored-By: Claude <noreply@anthropic.com>
* Keep readable workspace members' requires in Go selection
read_workspace dropped every main module when any `use` member was
outside the root or unreadable, so a readable member requiring a higher
version no longer marked a workspace zip pin inert and vex attested a
replace go does not build (Bugbot, golang.rs:271). In a workspace the
members' requires only ever prove a replace inert, so the readable ones
still count when another member cannot be read.
Co-Authored-By: Claude <noreply@anthropic.com>
* Audit go.work Socket replaces' requirements against the workspace
audit only ran requirement_issues on each member's go.mod replaces. A
shared patch's Socket replace now lives in go.work, so a patched copy
that raises or adds a require was never compared to the consumers:
apply --check stayed clean and the #618 tidy warning never fired, and a
later -mod=readonly workspace build could fail on the missing sum line.
audit_work now checks those copies against the requirements every use
member lists, taking the highest version per module as the workspace's
single build list does.
Co-Authored-By: Claude <noreply@anthropic.com>
* Name go.work in the hosted-pin remedies
A shared module's hosted pin can now live in the root go.work, where
apply refuses it, but the refusal still told users to restore go.mod/
go.sum or run go mod tidy, neither of which drops a go.work replace or
rewrites go.work.sum. Name go.work/go.work.sum and go work sync when the
pin is there, and mention go work sync in the vendor takeover warnings.
Co-Authored-By: Claude <noreply@anthropic.com>
* Refuse Go pins an unread workspace graph cannot prove
Two Bugbot findings on the workspace model:
- A workspace whose module graph could not be read (Selection::Unknown)
still confirmed a hosted pin when our own replace already sat at the
patched version. The existing pin only says what we wrote, not what go
selects, so it must not count as proof there: a go.mod missing from the
module cache may raise the module and leave the pin inert.
- The in-memory engine dropped a go.work it could not parse and fell back
to module mode, while the disk reader reports Loaded::Invalid and the
rewriter refuses. Memory now reports the same Invalid workspace.
Co-Authored-By: Claude <noreply@anthropic.com>
* Load raised versions' go.mod in the workspace graph
Go's workspace-mode readModGraph keeps going after the first pass: it
loads the go.mod of each root's selected version, and of every version
MVS raises a loaded module's requirement to, until nothing changes.
read_graph_from only walked the versions required directly, so a raised
dependency's requirements never took part and selection could report a
pin as Selected while go builds a higher version (e.g. c requires a
v1.1.0, which requires m v1.5.0, over the app's m v1.2.3). VEX and
apply --check then attested a pin the build does not link.
Mirror the hasDepsInAll loop and add the final reviewer's fixture as a
regression test.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011f9aeNX95QWYUQxd8w8Y8U
* Attest Socket's go.work pin over a leftover member pin
workspace_audit reports a member go.mod Socket replace overridden by
Socket's own go.work replace of the same module@version as WorkHosted,
and vex dropped the purl on every workspace issue. But that go.work pin
is what the workspace builds (and it passes vex's own go.work selection
gate below), so omitting the purl hid a patch the build links, e.g.
after a host switch leaves a member pin behind (Bugbot on #1764).
Skip WorkHosted in the override gate and add a vex regression test.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011f9aeNX95QWYUQxd8w8Y8U
* Load the workspace graph at go's three pruning levels
read_graph_from queued every member requirement and every raised
version as pruned, so it could miss a higher version of the patched
module that go loads and report the pin as Selected. Go enqueues each
main module at its workspace level, which loads the member's
requirements at the member's own pruning: a go 1.16 member loads its
whole transitive graph. It enqueues raised versions at the workspace
level too, so their direct requirements always load. Mirror that, and
dedup as go's loading/loadingUnpruned maps do.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Keep the root go.mod replace when the root is a go.work member
When the project root is both a module and a `use` member of its
go.work, and another member shares a patched module, the Socket replace
went into go.work only and an existing root go.mod replace was removed.
Any build that doesn't read this go.work (GOWORK=off, CI or Docker
checkouts without go.work) then linked the upstream module, silently.
Write the replace into both files instead: go.work for the workspace,
the root go.mod for module-mode builds of the root. They never
conflict, since go skips a member's replace of a path go.work replaces.
This covers apply/vendor (ensure_replace_entry upserts both, the
symlink refusal checks both, the vendor ledger records both and its
in-sync hot path restores a missing go.mod mirror) and hosted redirects
(go.mod and go.sum are pinned beside go.work and go.work.sum). Revert
and rollback already drop Socket replaces from both files.
Co-Authored-By: Claude <noreply@anthropic.com>
* Treat a missing root go.mod mirror as drift
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>
* Restore a missing go.mod mirror without rebuilding the copy
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>
* Merge fix/golang-workspace-model into 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>
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 86457da commit eb75d20
21 files changed
Lines changed: 2378 additions & 664 deletions
File tree
- crates
- socket-patch-cli
- src/commands
- scan
- tests
- e2e_vex_lockfile
- vex
- socket-patch-core
- src
- crawlers
- formats
- hosted
- patch/redirect
- upstream
- vendor
- vex/discover
- tests/equivalence
- docs
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
1867 | 1867 | | |
1868 | 1868 | | |
1869 | 1869 | | |
1870 | | - | |
| 1870 | + | |
| 1871 | + | |
1871 | 1872 | | |
1872 | 1873 | | |
1873 | 1874 | | |
1874 | 1875 | | |
1875 | | - | |
| 1876 | + | |
1876 | 1877 | | |
1877 | 1878 | | |
1878 | 1879 | | |
| |||
4304 | 4305 | | |
4305 | 4306 | | |
4306 | 4307 | | |
| 4308 | + | |
| 4309 | + | |
4307 | 4310 | | |
4308 | 4311 | | |
4309 | 4312 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
958 | 958 | | |
959 | 959 | | |
960 | 960 | | |
961 | | - | |
| 961 | + | |
962 | 962 | | |
963 | 963 | | |
964 | 964 | | |
965 | 965 | | |
| 966 | + | |
| 967 | + | |
| 968 | + | |
| 969 | + | |
| 970 | + | |
| 971 | + | |
| 972 | + | |
| 973 | + | |
| 974 | + | |
966 | 975 | | |
967 | 976 | | |
968 | 977 | | |
| |||
993 | 1002 | | |
994 | 1003 | | |
995 | 1004 | | |
996 | | - | |
| 1005 | + | |
| 1006 | + | |
| 1007 | + | |
| 1008 | + | |
| 1009 | + | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
997 | 1013 | | |
998 | 1014 | | |
999 | 1015 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
981 | 981 | | |
982 | 982 | | |
983 | 983 | | |
984 | | - | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
985 | 987 | | |
986 | 988 | | |
987 | 989 | | |
| |||
1005 | 1007 | | |
1006 | 1008 | | |
1007 | 1009 | | |
| 1010 | + | |
| 1011 | + | |
| 1012 | + | |
| 1013 | + | |
| 1014 | + | |
1008 | 1015 | | |
1009 | 1016 | | |
1010 | 1017 | | |
| |||
0 commit comments