From 90ad98d41a7f0e688a0ad8256bdb13d5efb5a055 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 11:35:13 +0000 Subject: [PATCH 1/5] Cancel only superseded PR runs in CI A CI or compatibility run is now cancelled only when a newer push to the same pull request replaces it. Push, dispatch and scheduled runs always finish, so a manually dispatched run on the v5 base branch (its only CI verdict, since push CI runs on main alone) is no longer killed by a later dispatch or by the non-main cancel rule, and main keeps finishing its rust-cache saves. CI now groups PR runs by PR number, like the compatibility workflows already do. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp --- .github/workflows/bun-compatibility.yml | 6 +++--- .github/workflows/ci.yml | 11 ++++++----- .github/workflows/go-compatibility.yml | 2 +- .github/workflows/pdm-compatibility.yml | 2 +- .github/workflows/poetry-compatibility.yml | 2 +- .github/workflows/vlt-compatibility.yml | 6 +++--- 6 files changed, 15 insertions(+), 14 deletions(-) diff --git a/.github/workflows/bun-compatibility.yml b/.github/workflows/bun-compatibility.yml index d5982448..232a4f30 100644 --- a/.github/workflows/bun-compatibility.yml +++ b/.github/workflows/bun-compatibility.yml @@ -95,11 +95,11 @@ on: permissions: contents: read -# Supersede stale PR runs. The `main` guard is load-bearing: main runs are the -# ONLY rust-cache writers (save-if), so they must never be cancelled mid-save. +# Supersede stale PR runs only: main runs are the ONLY rust-cache writers +# (save-if), so push, dispatch and schedule runs are never cancelled mid-save. concurrency: group: bun-patch-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: CARGO_PROFILE_DEV_DEBUG: '0' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eb2d4722..bba0540d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,12 +17,13 @@ on: permissions: contents: read -# Supersede stale runs on force-push / rapid PR updates. The `main` guard is -# load-bearing: main runs are the ONLY rust-cache writers (save-if), so they -# must never be cancelled mid-save. +# A newer push to the same PR supersedes its older run; nothing else is +# cancelled. Push, dispatch and schedule runs always finish: main runs are +# the ONLY rust-cache writers (save-if) and must not die mid-save, and a +# dispatched base-branch run is the base's only CI verdict. concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + group: ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: clippy: diff --git a/.github/workflows/go-compatibility.yml b/.github/workflows/go-compatibility.yml index 18149fad..3f0e6b9b 100644 --- a/.github/workflows/go-compatibility.yml +++ b/.github/workflows/go-compatibility.yml @@ -30,7 +30,7 @@ permissions: concurrency: group: go-compat-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: SOCKET_NO_CONFIG: '1' diff --git a/.github/workflows/pdm-compatibility.yml b/.github/workflows/pdm-compatibility.yml index ef4401b6..b417cb72 100644 --- a/.github/workflows/pdm-compatibility.yml +++ b/.github/workflows/pdm-compatibility.yml @@ -48,7 +48,7 @@ permissions: concurrency: group: pdm-compat-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: SOCKET_NO_CONFIG: '1' diff --git a/.github/workflows/poetry-compatibility.yml b/.github/workflows/poetry-compatibility.yml index 49367717..c4b07934 100644 --- a/.github/workflows/poetry-compatibility.yml +++ b/.github/workflows/poetry-compatibility.yml @@ -40,7 +40,7 @@ permissions: concurrency: group: poetry-compat-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: true + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: SOCKET_NO_CONFIG: '1' diff --git a/.github/workflows/vlt-compatibility.yml b/.github/workflows/vlt-compatibility.yml index 6fd588bd..5c3397ff 100644 --- a/.github/workflows/vlt-compatibility.yml +++ b/.github/workflows/vlt-compatibility.yml @@ -117,11 +117,11 @@ on: permissions: contents: read -# Supersede stale PR runs; main runs are the only rust-cache writers, so they -# are never cancelled mid-save. +# Supersede stale PR runs only: main runs are the only rust-cache writers, so +# push, dispatch and schedule runs are never cancelled mid-save. concurrency: group: vlt-compat-${{ github.event.pull_request.number || github.ref }} - cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} env: CARGO_PROFILE_DEV_DEBUG: '0' From bd632f5983902ee2f244c2999fc693bb8abef1fc Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 12:17:49 +0000 Subject: [PATCH 2/5] Expect native path separators in scan headers scan_hosted_paths_run_once_per_project_directory compared the per-directory `== apps/a ==` header against a literal forward-slash path, but scan prints the directory glob matched, which Windows spells `apps\a`. The Windows test leg failed on this since 62f07c7, and because every e2e job waits on `test`, the whole e2e tier was skipped on v5 PRs. Build the expected header from path components. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp --- crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs b/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs index 72219731..c5e57a55 100644 --- a/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs +++ b/crates/socket-patch-cli/tests/covgap_commands_scan_mod.rs @@ -1484,8 +1484,9 @@ async fn scan_hosted_paths_run_once_per_project_directory() { let (code, stdout, stderr) = run_scan_human(tmp.path(), &mock.uri(), &["apps/*"]); assert_eq!(code, 0, "stdout={stdout}; stderr={stderr}"); - for app in ["apps/a", "apps/b"] { - let header = format!("== {} ==", std::path::Path::new(app).display()); + // glob rebuilds matches with the native separator (`apps\a` on Windows). + for app in ["a", "b"] { + let header = format!("== {} ==", Path::new("apps").join(app).display()); assert!(stdout.contains(&header), "missing {header:?}: {stdout}"); } assert_eq!(stdout.matches("Redirected 0 packages").count(), 2, "{stdout}"); From b9286f95e4e184dac60d60db958c2f232e3af9c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 13:02:36 +0000 Subject: [PATCH 3/5] Run the vlt agent get test in agent mode get defaults to hosted mode since 5e5f5ed, so the real-vlt get_and_remove leg ran a hosted get and found the installed copy unpatched (Absent, expected Patched). Pass --mode agent, as the other agent-mode fixtures already do. Same change as cc5f1b6 on #285; it blocked the e2e tier's e2e_vlt jobs now that they run. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp --- crates/socket-patch-cli/tests/e2e_vlt.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/socket-patch-cli/tests/e2e_vlt.rs b/crates/socket-patch-cli/tests/e2e_vlt.rs index dc9e45e5..7d1dcb0e 100644 --- a/crates/socket-patch-cli/tests/e2e_vlt.rs +++ b/crates/socket-patch-cli/tests/e2e_vlt.rs @@ -151,7 +151,7 @@ async fn vlt_pinned_matrix_agent_get_and_remove() { return; }; let fx = Fixture::build(leg, Shape::with_bystander().warm()).await; - let out = socket_api(&fx.proj, &fx.svc, &["get", UUID], &[]); + let out = socket_api(&fx.proj, &fx.svc, &["get", UUID, "--mode", "agent"], &[]); assert_eq!(out.code, 0, "{out}"); assert_eq!(state(&fx.proj, fx.t()), State::Patched); let purl = fx.t().purl(); From c699754b93c926f7fddae689d452df0389fd9057 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 13:05:30 +0000 Subject: [PATCH 4/5] Run the pnpm safety e2e gets in agent mode get defaults to hosted mode since 5e5f5ed, so the three pnpm safety tests ran a hosted redirect and found proj_a's installed copy unpatched and no pnpm-layout note. They test the in-place apply path, so pass --mode agent. These e2e-tier tests had not run since that change because a red base test skipped the tier. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp --- crates/socket-patch-cli/tests/e2e_safety_pnpm.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/socket-patch-cli/tests/e2e_safety_pnpm.rs b/crates/socket-patch-cli/tests/e2e_safety_pnpm.rs index 26af7beb..7af95861 100644 --- a/crates/socket-patch-cli/tests/e2e_safety_pnpm.rs +++ b/crates/socket-patch-cli/tests/e2e_safety_pnpm.rs @@ -293,7 +293,7 @@ fn apply_in_a_does_not_mutate_b_or_store() { }; // -- get + apply in proj_a only ---------------------------------- - assert_run_ok(&fx.proj_a, &["get", NPM_UUID], "socket-patch get"); + assert_run_ok(&fx.proj_a, &["get", NPM_UUID, "--mode", "agent"], "socket-patch get"); // proj_a is patched. assert_eq!( @@ -397,7 +397,7 @@ fn pnpm_install_in_b_does_not_revert_a() { store_id }; - assert_run_ok(&fx.proj_a, &["get", NPM_UUID], "socket-patch get"); + assert_run_ok(&fx.proj_a, &["get", NPM_UUID, "--mode", "agent"], "socket-patch get"); assert_eq!(git_sha256_file(&index_a), AFTER_HASH); // Re-run pnpm install in proj_b with frozen lockfile — this @@ -475,7 +475,7 @@ fn apply_in_pnpm_project_emits_layout_note() { let root = tempfile::tempdir().unwrap(); let fx = setup_two_pnpm_projects(root.path()); - let (_stdout, stderr) = assert_run_ok(&fx.proj_a, &["get", NPM_UUID], "socket-patch get"); + let (_stdout, stderr) = assert_run_ok(&fx.proj_a, &["get", NPM_UUID, "--mode", "agent"], "socket-patch get"); // The exact phrasing is a stable contract. A bare `contains("pnpm")` // is worthless here — every pnpm store path printed on stderr From 10a772477a4e74b35680ec161e672d822bedaa59 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 13:08:16 +0000 Subject: [PATCH 5/5] Let the NuGet hosted e2e run without .socket/ v5 hosted mode writes no `.socket/` directory (the lock pins are the whole hosted state), so the hosted leg's fresh_checkout panicked with NotFound copying a tree that no longer exists. Copy it only when the run left one; the vendored leg still carries its ledger through. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01AfGnDQMy2FvpExneXmR1qp --- crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs b/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs index 9b4ccd72..6cd2272e 100644 --- a/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs +++ b/crates/socket-patch-cli/tests/e2e_nuget_dotnet_build.rs @@ -306,7 +306,10 @@ fn fresh_checkout(from: &Path, to: &Path) { for f in ["app.csproj", "nuget.config", "packages.lock.json"] { std::fs::copy(from.join(f), to.join(f)).unwrap_or_else(|e| panic!("copy {f}: {e}")); } - copy_tree(&from.join(".socket"), &to.join(".socket")); + // v5 hosted mode writes no `.socket/`: the lock pins are its whole state. + if from.join(".socket").is_dir() { + copy_tree(&from.join(".socket"), &to.join(".socket")); + } strip_manifest(to); let blobs = to.join(".socket/blobs"); if blobs.exists() {