Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b071e8a
Keep Go hosted pins after go mod tidy drops zip line
mikolalysenko Oct 27, 2026
a1bfe6e
Check selection and workspace sums for tidy Go pins
mikolalysenko Oct 27, 2026
14057c8
Document tidy Go pin rules in the CLI contract
mikolalysenko Oct 27, 2026
14fa6d4
Don't attest Go pins a workspace never builds
mikolalysenko Oct 27, 2026
b23666d
Let a hosted Go re-run restore a pruned zip line
mikolalysenko Oct 27, 2026
46b40a1
Don't attest Go /go.mod-only pins in a workspace
mikolalysenko Oct 27, 2026
bc5f8c4
Ignore go.work.sum zip lines as selection proof
mikolalysenko Oct 28, 2026
2beda00
Check workspace Zip pins for a higher selected version
claude Oct 28, 2026
801e0c6
Read Go workspaces the way go does
mikolalysenko Oct 27, 2026
db13d81
Check go.work before writing hosted Go pins
mikolalysenko Oct 27, 2026
6a54b8b
Roll back hosted Go pins in go.work and members
mikolalysenko Oct 27, 2026
46f86dc
Test and document Go workspace support
mikolalysenko Oct 27, 2026
b796800
Stop attesting Go workspace pins go may not build
mikolalysenko Oct 28, 2026
cbb852b
Select Go workspace versions from the module graph
mikolalysenko Oct 28, 2026
052a4fa
Read Go workspace e2e warnings via common::envelope
mikolalysenko Oct 28, 2026
5132bb0
Accept the workspace inert wording in zip pin test
mikolalysenko Oct 28, 2026
6862138
Gate go.work replaces on the workspace module graph
claude Oct 28, 2026
f534ec7
Keep readable workspace members' requires in Go selection
claude Oct 28, 2026
fd0c850
Merge remote-tracking branch 'origin/fix/golang-tidy-gomod-only-pin' …
claude Oct 28, 2026
54fb3eb
Refuse Go pins an unread workspace graph cannot prove
claude Oct 28, 2026
6dd327d
Load raised versions' go.mod in the workspace graph
claude Oct 28, 2026
5a2720f
Load the workspace graph at go's three pruning levels
claude Oct 28, 2026
678f90e
Merge main into fix/golang-workspace-model
claude Oct 28, 2026
ed7d030
Keep a workspace pin of a dependency-raised version live
claude Oct 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 26 additions & 13 deletions crates/socket-patch-cli/CLI_CONTRACT.md

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion crates/socket-patch-cli/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -359,7 +359,8 @@ async fn run_check(args: &ApplyArgs, manifest_path: &Path) -> i32 {
| GoDrift::StaleCopy { purl, .. }
| GoDrift::MissingReplace { purl }
| GoDrift::WrongReplacePath { purl, .. }
| GoDrift::ResolvedVersionMismatch { purl, .. } => purl.clone(),
| GoDrift::ResolvedVersionMismatch { purl, .. }
| GoDrift::UnprovenSelection { purl, .. } => purl.clone(),
GoDrift::OrphanReplace { module } => module.clone(),
};
drifts.push((id, "go_redirect_drift".to_string(), d.to_string()));
Expand Down
5 changes: 0 additions & 5 deletions crates/socket-patch-cli/src/commands/scan/hosted.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1358,10 +1358,6 @@ async fn run_redirect_selected_untracked(
// parent directory's and `$CARGO_HOME`'s): a directory source or local
// registry in any of them refuses the cargo rewrite like a project one.
let cargo_outer = || CargoCrawler::crates_io_source_for(&common.cwd);
// The other members of the go.work in effect: the workspace build
// selects the highest version any of them requires.
let go_workspace =
|| socket_patch_core::vendor::go_build_list::other_workspace_members(&common.cwd);
// The scan runs in the install's environment: a pnpm setting there
// (`pnpm_config_trust_lockfile`) overrides the project's files.
let pnpm_env = |var: &str| std::env::var(var).ok();
Expand All @@ -1379,7 +1375,6 @@ async fn run_redirect_selected_untracked(
npm_outer: &npm_outer,
yarn_classic_outer: &yarn_classic_outer,
cargo_outer: &cargo_outer,
go_workspace: &go_workspace,
pnpm_env: &pnpm_env,
blocking: true,
takeover_uuids: takeover_uuids.clone(),
Expand Down
53 changes: 40 additions & 13 deletions crates/socket-patch-cli/src/commands/vex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -986,24 +986,54 @@ async fn generate_vex(
// vendored or hosted) pinned to a version the build list does not select
// is inert, and the build links the unpatched module however intact the
// copy or pin (#391, #392, #509). go.mod + go.sum say which version the
// build selects; never attest a patch for another one.
// build selects, and in a go.work workspace the module graph; never
// attest a patch for another one, nor one a workspace may not select.
if !common.is_global() {
use socket_patch_core::utils::purl::build_golang_purl;
use socket_patch_core::utils::purl_key::PurlKey;
use socket_patch_core::vendor::go_build_list::{project_selection, Selection};
for entry in socket_patch_core::vendor::go_mod_edit::read_replace_entries(&common.cwd).await
let mut entries = Vec::new();
for root in socket_patch_core::vendor::go_workspace::module_roots(&common.cwd).await {
for entry in socket_patch_core::vendor::go_mod_edit::read_replace_entries(&root).await {
entries.push((root.clone(), entry));
}
}
// A go.work replace pins the whole workspace, and discovery attests
// it like a go.mod one, so it passes the same selection gate.
if let socket_patch_core::vendor::go_workspace::Loaded::Workspace(ws) =
socket_patch_core::vendor::go_workspace::load(&common.cwd).await
{
for entry in ws.replaces() {
entries.push((common.cwd.clone(), entry));
}
}
for (root, entry) in entries {
let Some(version) = entry.version.as_deref().filter(|_| entry.socket_owned()) else {
continue;
};
let wanted = PurlKey::new(&build_golang_purl(&entry.module, version));
if !outcome.applied.iter().any(|p| PurlKey::new(p) == wanted) {
continue;
}
let Some(Selection::Other(selected)) =
project_selection(&common.cwd, &entry.module, version).await
else {
continue;
Comment thread
mikolalysenko marked this conversation as resolved.
Resolved
let why = match project_selection(&root, &entry.module, version).await {
Some(Selection::Other(selected)) => format!(
"the build selects {} {selected}, so the replace pinned to {version} is \
ignored and the build links the unpatched module",
entry.module
),
Some(Selection::Unknown)
if socket_patch_core::vendor::go_workspace::resolve(&root)
!= socket_patch_core::vendor::go_workspace::GoWork::None =>
{
format!(
"the go.work workspace may build another version of {}: the go.mod \
of a module its build graph needs is in neither a replacement \
directory nor the module cache (`go mod download` fetches it), so \
nothing shows the replace pinned to {version} is the one go uses",
entry.module
)
}
_ => continue,
};
// Report each omission under the record's own spelling (`%2B`
// for `+incompatible`, #484), not go.mod's.
Expand All @@ -1017,11 +1047,7 @@ async fn generate_vex(
warnings,
common,
GO_VERSION_NOT_SELECTED,
format!(
"{purl}: the build selects {} {selected}, so the replace pinned to \
{version} is ignored and the build links the unpatched module",
entry.module
),
format!("{purl}: {why}"),
);
outcome.failed.push(FailedPatch {
purl,
Expand Down Expand Up @@ -2018,8 +2044,9 @@ fn omission_phrase(reason: &str) -> &'static str {
running it until it is rebuilt"
}
GO_VERSION_NOT_SELECTED => {
"go.mod and go.sum show the build selects another version of the module, so its \
version-pinned replace is ignored and the build links the unpatched module"
"go.mod and go.sum show the build selects another version of the module (or, in \
a go.work workspace, cannot show it selects the patched one), so its \
version-pinned replace may be ignored and the build link the unpatched module"
}
WIRING_CONFLICT => {
"the lockfiles wire this package to different patches, so which one the build \
Expand Down
23 changes: 17 additions & 6 deletions crates/socket-patch-cli/tests/e2e_golang_hosted_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1046,9 +1046,9 @@ async fn workspace_member_selecting_another_version_never_attests() {
/// A go.work member's dependency requires a higher version of the patched
/// module than any member's go.mod names. go builds that version without
/// asking for a go.mod update and writes its sums to `go.work.sum` itself,
/// so the `/go.mod`-only pin is never the attestation basis, before or
/// after the build. The higher zip line go then records proves nothing on
/// its own: go never drops it once the requirement is gone.
/// so the `/go.mod`-only pin is never the attestation basis, and once the
/// dependency's go.mod is in the module cache the replace is diagnosed
/// inert.
// #[serial]: see the capstone's note — env mutation vs env iteration.
#[tokio::test]
#[serial_test::serial]
Expand Down Expand Up @@ -1151,8 +1151,19 @@ async fn workspace_transitive_raise_never_attests() {
"{work_sum}"
);

std::env::set_var("GOMODCACHE", &machine.modcache);
let discovery = socket_patch_core::vex::discover::discover_patched_refs(&app).await;
let refs: Vec<_> = discovery.refs.iter().filter(|r| r.purl == purl).collect();
assert_eq!(refs.len(), 1, "{:?}", discovery.refs);
assert!(!refs[0].lockfile_basis_ok(), "{:?}", refs[0]);
std::env::remove_var("GOMODCACHE");
assert!(
!discovery.refs.iter().any(|r| r.purl == purl),
"{:?}",
discovery.refs
);
assert!(
discovery.diagnostics.iter().any(|d| d
.detail
.contains(&format!("is inert: the build selects {NEXT}"))),
"{:?}",
discovery.diagnostics
);
}
11 changes: 11 additions & 0 deletions crates/socket-patch-cli/tests/e2e_golang_hosted_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,17 @@ async fn hosted_pin_another_go_work_member_outranks_is_refused() {
let server = MockServer::start().await;
mount_hosted_grant(&server).await;
let modcache = tmp.path().join("modcache");
// The workspace's module graph reads each required version's go.mod
// from the module cache; without them the selection is unknown.
for v in [UVER, "v1.1.0"] {
let dir = modcache.join(format!("cache/download/{UMOD}/@v"));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join(format!("{v}.mod")),
format!("module {UMOD}\n\ngo 1.21\n"),
)
.unwrap();
}
let mismatch = |env: &serde_json::Value| {
env["warnings"]
.as_array()
Expand Down
Loading
Loading