Skip to content
Merged
Changes from all commits
Commits
Show all changes
33 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
22f8370
Write shared Go replaces into go.work
mikolalysenko Oct 27, 2026
61dddb3
Document go.work replaces for shared modules
mikolalysenko Oct 27, 2026
68e6445
Test one go.work replace for a shared module
mikolalysenko Oct 27, 2026
33cf080
Warn on stale vendor/ after a hosted go.work pin
mikolalysenko Oct 27, 2026
9a073d5
Fix Go workspace replace moves and rollbacks
mikolalysenko Oct 28, 2026
1874cc7
Refuse a go.work replace of a workspace module
mikolalysenko Oct 28, 2026
b7b1c61
Refuse to rewrite a symlinked go.work or go.mod
mikolalysenko Oct 28, 2026
78de435
Keep a missing final newline in go.mod and go.work
mikolalysenko Oct 28, 2026
d3f638b
Say when hosted rollback removes go.work.sum
mikolalysenko Oct 28, 2026
849332e
Fill the module cache in go.work vex fixtures
mikolalysenko Oct 28, 2026
4ec8bd2
Test go work vendor and where Go runs write
mikolalysenko Oct 27, 2026
83e3f1b
Pin go.work files in hosted redirect candidates
mikolalysenko Oct 28, 2026
faf7131
Fill the module cache before a hosted go work pin
mikolalysenko Oct 28, 2026
6862138
Gate go.work replaces on the workspace module graph
claude Oct 28, 2026
10c1aa9
Merge remote-tracking branch 'origin/fix/golang-workspace-model' into…
claude Oct 28, 2026
5770cd3
Merge commit '10c1aa9' into bd-1765
claude Oct 28, 2026
4a0e1fe
Merge main into test/golang-workspace-e2e
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
190 changes: 190 additions & 0 deletions crates/socket-patch-cli/tests/e2e_golang_workspace_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,196 @@ fn socket_raw(cwd: &Path, args: &[&str], modcache: &Path) -> (i32, String, Strin
)
}

// ── go work vendor, GOWORK elsewhere, member runs ─────────────────────

/// `go work vendor` (Go 1.22+) at `root`; its success.
fn work_vendor(tmp: &Path, root: &Path, proxy: &str) -> Output {
let m = Machine::new(tmp, &format!("work-vendor-{}", unique()));
go(root, &m, &["work", "vendor"], &day2(proxy))
}

/// A committed workspace `vendor/` directory: after vendor or hosted mode
/// replaces the shared module in go.work, the run warns that
/// `vendor/modules.txt` does not record it and the `-mod=vendor` build
/// fails as go says; `go work vendor` records it and the members build the
/// patched module from `vendor/` fully offline; `vendor --check` is clean
/// again.
#[test]
fn go_work_vendor_records_the_go_work_replace() {
if !golang_e2e_matrix::toolchain_ready("e2e_golang_workspace_build")
|| golang_e2e_matrix::go_minor() < 22
{
return;
}
for mode in ["vendored", "hosted"] {
let tmp = tempfile::tempdir().unwrap();
let _guard = ChmodGuard(tmp.path().to_path_buf());
let w = stage_members(tmp.path(), UVER, UVER);
let out = work_vendor(tmp.path(), &w.root, &w.proxy);
assert!(
out.status.success(),
"{mode}: {}",
String::from_utf8_lossy(&out.stderr)
);
let offline_vendor = [
("GOPROXY", "off"),
("GOSUMDB", "off"),
("GOFLAGS", "-mod=vendor"),
];
let (out, err) = run_member(tmp.path(), &w.root, "a", "off", &offline_vendor);
assert!(out.contains("A: PRISTINE"), "{mode} baseline: {out}\n{err}");

let m = Machine::new(tmp.path(), "run");
let env = if mode == "vendored" {
download(&w.root, &m, &w.proxy);
write_local_patch(&w.root, VENDOR_UUID);
let cwd = w.root.to_str().unwrap();
let (code, env, stderr) = socket(
&w.root,
&["vendor", "--json", "--offline", "--cwd", cwd],
&m.modcache,
);
assert_eq!(code, 0, "{env}\n{stderr}");
env
} else {
warm(tmp.path(), &m, &w.proxy, &[(UMOD, UVER)]);
let (code, env, stderr) = w.api.get(&w.root, &m.modcache, &[]);
assert_eq!(code, 0, "{env}\n{stderr}");
env
};
assert!(
warning_codes(&env).contains(&"go_vendor_modules_txt_out_of_sync".to_string()),
"{mode}: the run names `go work vendor`: {env}"
);
let (out, err) = run_member(tmp.path(), &w.root, "a", "off", &offline_vendor);
assert!(
!out.contains("PATCHED") && err.contains("inconsistent vendoring"),
"{mode}: {out}\n{err}"
);

let out = work_vendor(tmp.path(), &w.root, &w.proxy);
assert!(
out.status.success(),
"{mode}: {}",
String::from_utf8_lossy(&out.stderr)
);
let modules_txt = read(&w.root.join("vendor/modules.txt"));
assert!(
modules_txt
.lines()
.any(|l| l.starts_with(&format!("# {UMOD} {UVER} => "))),
"{mode}: {modules_txt}"
);
for dir in ["a", "b"] {
let (out, err) = run_member(tmp.path(), &w.root, dir, "off", &offline_vendor);
assert!(
out.contains(&format!("{}: PATCHED", dir.to_uppercase())),
"{mode} {dir}: {out}\n{err}"
);
}
if mode == "vendored" {
let cwd = w.root.to_str().unwrap();
let (code, env, stderr) = socket(
&w.root,
&["vendor", "--check", "--json", "--cwd", cwd],
&m.modcache,
);
assert_eq!(code, 0, "vendor --check: {env}\n{stderr}");
}
}
}

/// Where the run happens decides where the replace goes. From a member
/// (go.work above it) `vendor` writes the member's go.mod, and a second
/// member is refused with the way out: run at the workspace root. From the
/// root the shared module is replaced once in go.work, over the member's
/// own Socket replace, and both members build it. `GOWORK` naming the same
/// file by absolute path is the same workspace; one naming a go.work
/// elsewhere leaves the root with no module and no go.work of its own, so
/// the run refuses and writes nothing.
#[test]
fn run_location_and_gowork_decide_the_replace_file() {
if !golang_e2e_matrix::toolchain_ready("e2e_golang_workspace_build")
|| !golang_e2e_matrix::supports_go_work()
{
return;
}
let tmp = tempfile::tempdir().unwrap();
let _guard = ChmodGuard(tmp.path().to_path_buf());
let w = stage_members(tmp.path(), UVER, UVER);
let m = Machine::new(tmp.path(), "vendor");
download(&w.root, &m, &w.proxy);
let (a, b) = (w.root.join("a"), w.root.join("b"));
write_local_patch(&a, VENDOR_UUID);
write_local_patch(&b, VENDOR_UUID);
write_local_patch(&w.root, VENDOR_UUID);
let vendor = |cwd: &Path, env: &[(&str, &str)]| {
socket_env(
cwd,
&[
"vendor",
"--json",
"--offline",
"--cwd",
cwd.to_str().unwrap(),
],
&m.modcache,
env,
)
};

let (code, env, stderr) = vendor(&a, &[]);
assert_eq!(code, 0, "{env}\n{stderr}");
let art = format!(".socket/vendor/golang/{VENDOR_UUID}/{UMOD}@{UVER}");
assert!(
read(&a.join("go.mod")).contains(&format!("replace {UMOD} {UVER} => ./{art}")),
"a member run writes its own go.mod"
);
let b_mod = read(&b.join("go.mod"));
let (code, env, _) = vendor(&b, &[]);
assert_ne!(code, 0, "{env}");
let text = env.to_string();
assert!(
text.contains("conflicting replacements") && text.contains("run socket-patch in"),
"{env}"
);
assert_eq!(read(&b.join("go.mod")), b_mod, "nothing written in b");

let work_before = read(&w.root.join("go.work"));
let elsewhere = tmp.path().join("other");
std::fs::create_dir_all(&elsewhere).unwrap();
std::fs::write(
elsewhere.join("go.work"),
format!(
"go 1.18\n\nuse (\n\t{}\n\t{}\n)\n",
a.display(),
b.display()
),
)
.unwrap();
let gowork = elsewhere.join("go.work");
let (code, env, _) = vendor(&w.root, &[("GOWORK", gowork.to_str().unwrap())]);
assert_ne!(code, 0, "{env}");
assert!(env.to_string().contains("has nowhere to go"), "{env}");
assert_eq!(read(&w.root.join("go.work")), work_before);

let own = w.root.join("go.work");
let (code, env, stderr) = vendor(&w.root, &[("GOWORK", own.to_str().unwrap())]);
assert_eq!(code, 0, "{env}\n{stderr}");
assert_eq!(
read(&w.root.join("go.work")),
format!("{work_before}\nreplace {UMOD} {UVER} => ./{art}\n")
);
let offline = [("GOPROXY", "off"), ("GOSUMDB", "off")];
for dir in ["a", "b"] {
let (out, err) = run_member(tmp.path(), &w.root, dir, "off", &offline);
assert!(
out.contains(&format!("{}: PATCHED", dir.to_uppercase())),
"{dir}: {out}\n{err}"
);
}
}

/// go keeps no member's go.mod tidy in workspace mode: `b` requires `x`,
/// which requires `UMOD` v1.1.0, so the workspace builds v1.1.0 although
/// every member requires v1.0.0, and only x's go.mod shows it. Until the
Expand Down
Loading