Skip to content

Commit 2beda00

Browse files
committed
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>
1 parent bc5f8c4 commit 2beda00

1 file changed

Lines changed: 75 additions & 24 deletions

File tree

  • crates/socket-patch-core/src/vex/discover

‎crates/socket-patch-core/src/vex/discover/golang.rs‎

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -345,31 +345,40 @@ fn extract_file(
345345
let Some(purl) = replaced_purl(entry, required, &versioned, file, out) else {
346346
continue;
347347
};
348-
let (integrity, integrity_required) = match sums.pin(rhs, rhs_version) {
349-
SumPin::Zip(h1) => (Some(LockIntegrity::GoH1(h1)), true),
350-
SumPin::GoModOnly => {
351-
// MVS reads (and go.sum keeps the `/go.mod` line of)
352-
// versions it does not select; only a zip line shows
353-
// the replaced version is the one built.
354-
let req = required.get(&entry.module).map(String::as_str);
355-
// `replaced_purl` resolved one of the two.
356-
let version = entry.version.as_deref().or(req).unwrap_or_default();
357-
let zips = sums.zip_versions(&entry.module);
358-
let selected = graph.select(&entry.module, version, &zips);
359-
if let Some(other) = selected.other_version() {
360-
out.diag(
361-
DIAG_REF_INVALID,
362-
file,
363-
format!(
364-
"{file}: replace {} {version} is inert: the build \
365-
selects {other}, so it uses the unpatched module",
366-
entry.module
367-
),
368-
);
369-
continue;
370-
}
371-
(None, graph.workspace)
348+
let pin = sums.pin(rhs, rhs_version);
349+
// MVS reads (and go.sum keeps the `/go.mod` line of) versions
350+
// it does not select, so a `/go.mod`-only pin needs proof the
351+
// replaced version is the one built. In a workspace a zip
352+
// line is no proof either: a sum file is only appended to, so
353+
// a member that later raises the module leaves the socket zip
354+
// line in place while go builds the higher version.
355+
let check = match pin {
356+
SumPin::GoModOnly => true,
357+
SumPin::Zip(_) => graph.workspace,
358+
SumPin::Unpinned => false,
359+
};
360+
if check {
361+
let req = required.get(&entry.module).map(String::as_str);
362+
// `replaced_purl` resolved one of the two.
363+
let version = entry.version.as_deref().or(req).unwrap_or_default();
364+
let zips = sums.zip_versions(&entry.module);
365+
let selected = graph.select(&entry.module, version, &zips);
366+
if let Some(other) = selected.other_version() {
367+
out.diag(
368+
DIAG_REF_INVALID,
369+
file,
370+
format!(
371+
"{file}: replace {} {version} is inert: the build \
372+
selects {other}, so it uses the unpatched module",
373+
entry.module
374+
),
375+
);
376+
continue;
372377
}
378+
}
379+
let (integrity, integrity_required) = match pin {
380+
SumPin::Zip(h1) => (Some(LockIntegrity::GoH1(h1)), true),
381+
SumPin::GoModOnly => (None, graph.workspace),
373382
SumPin::Unpinned => (None, true),
374383
};
375384
let url = format!("{rhs}@{rhs_version}");
@@ -1168,6 +1177,48 @@ mod tests {
11681177
}
11691178
}
11701179

1180+
/// A workspace's sum files are only appended to, so the socket zip line
1181+
/// can outlive the build that wrote it: a member that requires a higher
1182+
/// version makes even a zip-pinned replace inert.
1183+
#[tokio::test]
1184+
async fn workspace_zip_pin_is_inert_under_a_higher_member_require() {
1185+
let m = gopatch(UUID_A);
1186+
let sver = "v1.4.2-socketpatch.1";
1187+
for (member_version, inert) in [(VERSION, false), ("v1.4.1", false), ("v1.5.0", true)] {
1188+
let p = Project::new();
1189+
p.write("go.mod", BASE_GO_MOD)
1190+
.write(
1191+
"go.work",
1192+
format!(
1193+
"go 1.21\n\nuse (\n\t.\n\t./svc\n)\n\nreplace {MODULE} {VERSION} => {m} {sver}\n"
1194+
),
1195+
)
1196+
.write(
1197+
"go.work.sum",
1198+
format!("{m} {sver} {ZIP_H1}\n{m} {sver}/go.mod {MOD_H1}\n"),
1199+
)
1200+
.write(
1201+
"svc/go.mod",
1202+
format!("module example.com/svc\n\ngo 1.21\n\nrequire {MODULE} {member_version}\n"),
1203+
);
1204+
let out = run(&p).await;
1205+
if inert {
1206+
assert_refs(&out, &[]);
1207+
assert_eq!(out.diagnostics.len(), 1, "{member_version}");
1208+
assert_eq!(out.diagnostics[0].code, DIAG_REF_INVALID);
1209+
assert!(out.diagnostics[0].detail.contains("selects v1.5.0"));
1210+
} else {
1211+
assert_refs(&out, &[(PURL, UUID_A, WiringMode::Hosted)]);
1212+
assert_eq!(
1213+
out.refs[0].locked_integrity,
1214+
Some(LockIntegrity::GoH1(ZIP_H1.to_string())),
1215+
"{member_version}"
1216+
);
1217+
assert!(out.diagnostics.is_empty(), "{:?}", out.diagnostics);
1218+
}
1219+
}
1220+
}
1221+
11711222
/// go verifies against the sum files of the mode it builds in: in
11721223
/// workspace mode `go.work.sum` and the members' go.sum (the root's only
11731224
/// when `.` is a member); under `GOWORK=off` the root go.sum alone, with

0 commit comments

Comments
 (0)