Skip to content

Commit 32fb4f0

Browse files
mikolalysenkoclaude
andcommitted
Warn when Go vendor/ or go.mod need a resync
Two Go build breakages were reported as success. #343: in a project with a committed vendor/ (go mod vendor), wiring a socket `replace` (apply, vendor, scan --mode hosted) or removing one (rollback) leaves vendor/modules.txt out of step with go.mod, so every default build fails with "inconsistent vendoring". #618: when a patch bumps or adds a requirement in the patched module's own go.mod, the consumer's go.mod/go.sum are not updated, so the default -mod=readonly build fails with "updates to go.mod needed". In both cases apply exited 0 and apply --check said in sync. A new vendor::go_consumer_sync::audit compares the replace directives with vendor/modules.txt (go mod vendor / go work vendor layouts) and with the patched copies' go.mod requirements. apply, vendor, rollback and hosted scan now emit go_vendor_modules_txt_out_of_sync / go_requirements_out_of_sync warnings that name the go command to run. apply --check and vendor --check report them as drift until it is run. Fixes #343 Fixes #618 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1 parent a8e9397 commit 32fb4f0

10 files changed

Lines changed: 982 additions & 4 deletions

File tree

‎crates/socket-patch-cli/src/commands/apply.rs‎

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,26 @@ async fn reconcile_local_go(common: &GlobalArgs, target_manifest_purls: &HashSet
523523
}
524524
}
525525

526+
/// The consumer-sync audit ([`go_consumer_sync::audit`]) of the project at
527+
/// `root` as run warnings: a committed `vendor/modules.txt` or a go.mod
528+
/// requirement set that no longer matches the socket `replace` directives,
529+
/// each naming the go command that fixes it.
530+
///
531+
/// [`go_consumer_sync::audit`]: socket_patch_core::vendor::go_consumer_sync::audit
532+
pub(crate) async fn go_consumer_sync_warnings(
533+
root: &Path,
534+
pristine_go_mods: &HashMap<String, PathBuf>,
535+
) -> Vec<RunWarning> {
536+
socket_patch_core::vendor::go_consumer_sync::audit(root, pristine_go_mods)
537+
.await
538+
.into_iter()
539+
.map(|issue| RunWarning {
540+
code: issue.code().to_string(),
541+
detail: issue.to_string(),
542+
})
543+
.collect()
544+
}
545+
526546
/// Read-only verification that the manifest's patches are in place, for CI
527547
/// / GitHub-App auditing. Lock-free, fetch-free, offline-safe, and it never
528548
/// writes. Exits 0 when in sync, 1 on drift.
@@ -608,6 +628,21 @@ async fn run_check(args: &ApplyArgs, manifest_path: &Path) -> i32 {
608628
drifts.push((id, "go_redirect_drift".to_string(), d.to_string()));
609629
}
610630
}
631+
// The redirects can be intact while the project no longer
632+
// builds: a committed vendor/modules.txt or the go.mod
633+
// requirements out of step with them (#343, #618).
634+
for issue in socket_patch_core::vendor::go_consumer_sync::audit(
635+
&args.common.cwd,
636+
&HashMap::new(),
637+
)
638+
.await
639+
{
640+
drifts.push((
641+
issue.module().to_string(),
642+
issue.code().to_string(),
643+
issue.to_string(),
644+
));
645+
}
611646
}
612647
}
613648

@@ -676,7 +711,14 @@ async fn run_check(args: &ApplyArgs, manifest_path: &Path) -> i32 {
676711
for (_, _, detail) in &drifts {
677712
eprintln!(" {detail}");
678713
}
679-
eprintln!("Run `socket-patch apply` to regenerate them.");
714+
// A go consumer-sync drift names its own go command; apply
715+
// cannot regenerate vendor/modules.txt or go.sum.
716+
if drifts.iter().any(|(_, code, _)| {
717+
code != socket_patch_core::vendor::go_consumer_sync::VENDOR_MODULES_TXT_CODE
718+
&& code != socket_patch_core::vendor::go_consumer_sync::REQUIREMENTS_CODE
719+
}) {
720+
eprintln!("Run `socket-patch apply` to regenerate them.");
721+
}
680722
}
681723
1
682724
}
@@ -2228,6 +2270,7 @@ async fn apply_patches_inner(
22282270
});
22292271
}
22302272
let mut fallback_skips: Vec<FallbackHomeSkip> = Vec::new();
2273+
let mut go_pristine_mods: HashMap<String, PathBuf> = HashMap::new();
22312274

22322275
// Multi-copy aware: npm nests genuine duplicates of one `name@version`
22332276
// (nested dupes, diamonds, `file:` dups), so the resolver returns EVERY
@@ -2650,7 +2693,16 @@ async fn apply_patches_inner(
26502693
match try_local_go_apply(purl, pkg_path, patch, &sources, &args.common, policy)
26512694
.await
26522695
{
2653-
Some(r) => r,
2696+
Some(r) => {
2697+
// The unpatched go.mod, so the consumer-sync
2698+
// audit below can tell requirements the patch
2699+
// added from ones upstream always had (#618).
2700+
if let Some((module, _)) = parse_golang_purl(purl) {
2701+
go_pristine_mods
2702+
.insert(module.into_owned(), pkg_path.join("go.mod"));
2703+
}
2704+
r
2705+
}
26542706
None => {
26552707
apply_package_patch(
26562708
purl,
@@ -2727,6 +2779,13 @@ async fn apply_patches_inner(
27272779

27282780
// The human summary is printed by `run`, after the per-package list.
27292781

2782+
// A redirect leaves the project building only if its committed
2783+
// `vendor/modules.txt` and its go.mod requirements still agree with the
2784+
// `replace` directives (#343, #618): name the regeneration step.
2785+
if !args.common.dry_run && eco_in_local_scope(&args.common, Ecosystem::Golang) {
2786+
run_warnings.extend(go_consumer_sync_warnings(&args.common.cwd, &go_pristine_mods).await);
2787+
}
2788+
27302789
// Note: `apply` deliberately does NOT garbage-collect unused blobs in
27312790
// `.socket/`. GC is the responsibility of `socket-patch repair` /
27322791
// `gc` / `scan --prune`. Keeping apply read-only against `.socket/`

‎crates/socket-patch-cli/src/commands/rollback.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2660,6 +2660,19 @@ pub(crate) async fn rollback_patches_inner(
26602660
results.push(result);
26612661
}
26622662

2663+
// Dropping a `replace` leaves a committed vendor/modules.txt recording
2664+
// it, which breaks every vendored build until it is regenerated (#343).
2665+
if !common.dry_run
2666+
&& results
2667+
.iter()
2668+
.any(|r| r.success && is_local_go(&r.package_key, common))
2669+
{
2670+
let none = std::collections::HashMap::new();
2671+
for w in crate::commands::apply::go_consumer_sync_warnings(&common.cwd, &none).await {
2672+
warnings.push((w.code, w.detail));
2673+
}
2674+
}
2675+
26632676
superseded_left.sort();
26642677
superseded_left.dedup();
26652678
Ok(RollbackOutcome {

‎crates/socket-patch-cli/src/commands/scan/hosted.rs‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,26 @@ pub(crate) async fn run_redirect_selected(
15751575
engine_warnings.extend(done.rush_warnings.iter().cloned());
15761576
engine_warnings.extend(done.pnpm_warnings.iter().cloned());
15771577
engine_warnings.extend(done.npm_warnings.iter().cloned());
1578+
// A hosted Go `replace` breaks a committed vendor/ directory until
1579+
// `go mod vendor` re-records it in vendor/modules.txt (#343).
1580+
if !common.dry_run
1581+
&& done
1582+
.rewritten
1583+
.iter()
1584+
.any(|f| f == "go.mod" || f.ends_with("/go.mod"))
1585+
{
1586+
for w in crate::commands::apply::go_consumer_sync_warnings(
1587+
&common.cwd,
1588+
&std::collections::HashMap::new(),
1589+
)
1590+
.await
1591+
{
1592+
engine_warnings.push(socket_patch_core::patch::redirect::RewriteWarning {
1593+
code: w.code,
1594+
detail: w.detail,
1595+
});
1596+
}
1597+
}
15781598
let mut warnings: Vec<serde_json::Value> =
15791599
socket_patch_core::hosted::render::rewrite_warnings_json(&engine_warnings);
15801600
warnings.extend(gem_stale.warnings.iter().cloned());

‎crates/socket-patch-cli/src/commands/vendor.rs‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,6 +1236,7 @@ async fn run_check(args: &VendorArgs) -> i32 {
12361236
} else {
12371237
None
12381238
};
1239+
let go_sync_issues = tokio::sync::OnceCell::new();
12391240
for (key, entry) in entries {
12401241
let record = entry.record.as_ref().or_else(|| manifest.patches.get(key));
12411242
let mut failure = match record {
@@ -1268,6 +1269,26 @@ async fn run_check(args: &VendorArgs) -> i32 {
12681269
failure = Some(unwired_check_failure(discovery, root, key, entry).await);
12691270
}
12701271
}
1272+
// The committed copy and its `replace` can be intact while the
1273+
// project no longer builds: vendor/modules.txt or the go.mod
1274+
// requirements out of step with the `replace` (#343, #618).
1275+
if failure.is_none() && entry.ecosystem == "golang" {
1276+
if let Some((module, _)) = socket_patch_core::utils::purl::parse_golang_purl(key) {
1277+
let issues: Vec<String> = go_sync_issues
1278+
.get_or_init(|| async {
1279+
socket_patch_core::vendor::go_consumer_sync::audit(root, &HashMap::new())
1280+
.await
1281+
})
1282+
.await
1283+
.iter()
1284+
.filter(|issue| issue.module() == module)
1285+
.map(ToString::to_string)
1286+
.collect();
1287+
if !issues.is_empty() {
1288+
failure = Some(issues.join("; "));
1289+
}
1290+
}
1291+
}
12711292
if vendor::jvm::apply::upstream_unverified(entry) {
12721293
env.warnings.push(RunWarning {code: "vendor_jvm_upstream_unverified".into(), detail: format!("{key}: upstream metadata was accepted offline; run vendor online to verify registry checksums")});
12731294
}
@@ -3778,6 +3799,19 @@ pub(crate) async fn vendor_records_reusing(
37783799
}
37793800
}
37803801

3802+
// A vendored Go module builds only if the committed vendor/modules.txt
3803+
// and the go.mod requirements agree with its `replace` (#343, #618).
3804+
if !common.dry_run && records.keys().any(|p| p.starts_with("pkg:golang/")) {
3805+
for w in
3806+
crate::commands::apply::go_consumer_sync_warnings(&common.cwd, &HashMap::new()).await
3807+
{
3808+
if !common.json && !common.silent {
3809+
eprintln!("Warning: {}", w.detail);
3810+
}
3811+
env.warnings.push(w);
3812+
}
3813+
}
3814+
37813815
// A rolled-back eject's summary would describe a vendoring that was
37823816
// undone: the eject prints it itself once it knows the outcome (#1005).
37833817
match eject {

0 commit comments

Comments
 (0)