Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
9b898b1
feat(apply): safety primitives — lock, CoW, atomic write, sidecar fixups
mikolalysenko May 22, 2026
39a2321
feat(cli): wire safety primitives + Maven/NuGet experimental gates
mikolalysenko May 22, 2026
13cbfa7
test(e2e): safety hardening suite + CI matrix + invariant fixups
mikolalysenko May 22, 2026
6dc3218
refactor(sidecars): typed envelope contract with structured per-file …
mikolalysenko May 22, 2026
2daa5ac
test(e2e): expand sidecar coverage + simplify PTY harness
mikolalysenko May 22, 2026
2b95558
test(e2e): close remaining cargo + nuget sidecar fixup-error arms
mikolalysenko May 22, 2026
2d82fac
test(e2e): close internals guards + nuget non-UTF8 iteration arm
mikolalysenko May 22, 2026
5c9a5fb
test(e2e): exercise sidecar/cow defensive arms via direct dispatch
mikolalysenko May 22, 2026
09ecc10
test(e2e): cover cow.rs symlink/hardlink/stage-write error arms
mikolalysenko May 22, 2026
f7d916d
refactor(sidecars,cow): collapse two dead-arm Result paths
mikolalysenko May 22, 2026
fbbc05c
refactor(nuget,cow): byte-suffix match + ACL test → 100% region cov
mikolalysenko May 22, 2026
4e2f3a1
chore(cleanup): remove dead manifest::recovery + fuzzy_match exports
mikolalysenko May 22, 2026
b7c4cca
chore(cleanup): purge dead utils::purl exports + duplicated tests
mikolalysenko May 22, 2026
09c9115
chore(cleanup): purge dead envelope builders + summary byte counters
mikolalysenko May 22, 2026
4a6d372
test(core): integration coverage for diff + package + fuzzy_match
mikolalysenko May 22, 2026
89eb3c1
chore(cleanup): remove dead Ecosystem::purl_prefix + manifest helpers
mikolalysenko May 22, 2026
9dabcb9
chore(cleanup): remove test-only pub helpers (nuspec parser, multi-up…
mikolalysenko May 22, 2026
90d2b67
chore(cleanup): drop duplicate utils::purl::build_npm_purl
mikolalysenko May 22, 2026
8d71ea1
chore(cleanup): drop dead utils::env_compat::read_env_either
mikolalysenko May 22, 2026
c8b7989
chore(cleanup): remove 4 unused .socket/* constants
mikolalysenko May 22, 2026
6c5d393
chore(cleanup): drop dead telemetry::track_patch_event_fire_and_forget
mikolalysenko May 22, 2026
b465992
test(core): integration coverage for rollback new-file + error paths
mikolalysenko May 22, 2026
0c2bcb2
test(core): integration coverage for blob_fetcher early-return paths
mikolalysenko May 22, 2026
2b2b4bf
chore(cleanup): silence test-only warnings (unused fixtures + stray a…
mikolalysenko May 22, 2026
6d3dc8e
test(repair): cover --offline + --download-only mutual exclusion
mikolalysenko May 22, 2026
8843e67
test(apply): cover no-.socket-dir status: noManifest envelope
mikolalysenko May 22, 2026
fba169a
test(get): cover UUID-by-UUID paid-required path on public proxy
mikolalysenko May 22, 2026
e39b95b
test(get): batch coverage for get.rs envelope shapes
mikolalysenko May 22, 2026
edc6803
test(cli): batch --dry-run + empty-manifest path coverage
mikolalysenko May 22, 2026
8e3d042
test(output): integration coverage for ANSI color helpers
mikolalysenko May 22, 2026
a3ebc75
test(blob_fetcher): cover fetch_blobs_by_hash skip-existing branch
mikolalysenko May 22, 2026
8fe8939
test(blob_fetcher): expand to 9 tests covering DownloadMode + sources
mikolalysenko May 22, 2026
abc5b44
test(crawlers): empty/missing path early-returns for NpmCrawler
mikolalysenko May 22, 2026
fa3421a
test(crawlers): empty-purl/empty-path branches across all 7 ecosystems
mikolalysenko May 22, 2026
095377c
test(telemetry): integration coverage for is_telemetry_disabled + san…
mikolalysenko May 22, 2026
d01478f
refactor(crawlers): runtime cfg!() to compile-time #[cfg(...)] gates
mikolalysenko May 23, 2026
690e648
test(crawler/python): 14 integration tests for find_python_dirs + ven…
mikolalysenko May 23, 2026
bd2ca92
test(crawler/nuget): 15 integration tests for find_by_purls + crawl_a…
mikolalysenko May 23, 2026
d65d2f7
test(crawler/ruby): 13 integration tests for find_by_purls + get_gem_…
mikolalysenko May 23, 2026
3765c93
test(crawler/maven): 16 integration tests for parse_pom + find_by_pur…
mikolalysenko May 23, 2026
3bcbf31
test(crawler/composer): 12 integration tests for vendor + installed.j…
mikolalysenko May 23, 2026
73b4f40
test(crawler/cargo): 14 integration tests for parse_cargo_toml + find…
mikolalysenko May 23, 2026
0a186ea
test(crawler/go): 14 integration tests for encode/decode/parse + paths
mikolalysenko May 23, 2026
05f226d
test(crawler/cargo): +3 tests for parse_dir_name_version fallback
mikolalysenko May 23, 2026
dc36eab
test(crawler/npm): 17 integration tests for npm crawler
mikolalysenko May 23, 2026
cb96d0d
chore(crawlers): drop dead NpmPkgManager::as_tag + extend coverage
mikolalysenko May 23, 2026
9e110de
test(crawlers): more npm + composer coverage
mikolalysenko May 23, 2026
0f3c39b
test(crawlers): maven + nuget + ruby + go coverage
mikolalysenko May 23, 2026
97513c9
test(crawlers): python + cargo coverage
mikolalysenko May 23, 2026
83016b8
test(crawlers): deeper npm scope/nested + CrawlerOptions default
mikolalysenko May 23, 2026
f1b0474
test(crawlers): maven + go env-fallback coverage
mikolalysenko May 23, 2026
9568eae
test(crawlers): fix python METADATA blank-line break test
mikolalysenko May 23, 2026
9e82aa4
test(crawlers): npm shell-out wrappers via PATH stubbing
mikolalysenko May 23, 2026
0856547
test(crawlers): composer/ruby/nuget shell-out + edge coverage
mikolalysenko May 23, 2026
c988d98
test(crawlers): maven + cargo final coverage
mikolalysenko May 23, 2026
e8d815c
test(crawlers): chmod-based unreadable-dir coverage across crawlers
mikolalysenko May 23, 2026
7aa479a
test(crawlers): extract parse_composer_home_output for unit testing
mikolalysenko May 23, 2026
8084862
refactor(crawlers): centralize read_dir/file_type behind utils::fs
mikolalysenko May 23, 2026
64b4325
refactor(crawlers): inject CommandRunner for npm/ruby/python shell-outs
mikolalysenko May 23, 2026
3f796f2
feat(crawlers/python): extensive uv support
mikolalysenko May 23, 2026
e485704
feat(crawlers): bun + deno scan/apply support
mikolalysenko May 23, 2026
166af51
feat(cli/crawlers): wire DenoCrawler into ecosystem dispatch + docker…
mikolalysenko May 23, 2026
68a9753
fix(clippy): inline nested doc list in deno_crawler module docstring
mikolalysenko May 23, 2026
8b0b9e8
fix(clippy): allow dead_code on find_node_dirs_sync for non-macOS tar…
mikolalysenko May 23, 2026
11576e3
fix(docker-e2e): pass experimental gate env vars for maven and nuget
mikolalysenko May 23, 2026
4e564f3
fix(types/tests): bump test_all_count expected for deno feature
mikolalysenko May 23, 2026
3c4857b
fix(tests): cross-platform fixes for windows + linux CI runners
mikolalysenko May 23, 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
45 changes: 39 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ jobs:
# separately, and coverage-merge stitches everything together.
run: |
cargo llvm-cov --workspace \
--features cargo,golang,maven,composer,nuget \
--features cargo,golang,maven,composer,nuget,deno \
--no-report
cargo llvm-cov report --lcov --output-path coverage-host.lcov
cargo llvm-cov report --summary-only | tee coverage-summary.txt
Expand Down Expand Up @@ -206,7 +206,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget]
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget, deno]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down Expand Up @@ -270,7 +270,7 @@ jobs:
# cargo llvm-cov manages its own env in the test step).
run: |
eval "$(cargo llvm-cov show-env --export-prefix 2>/dev/null)"
cargo build --bin socket-patch --features cargo,golang,maven,composer,nuget
cargo build --bin socket-patch --features cargo,golang,maven,composer,nuget,deno

- name: Configure docker-e2e coverage hooks
run: |
Expand All @@ -282,7 +282,7 @@ jobs:
- name: Run ${{ matrix.ecosystem }} Docker e2e test with coverage
run: |
cargo llvm-cov \
--features docker-e2e,cargo,golang,maven,composer,nuget \
--features docker-e2e,cargo,golang,maven,composer,nuget,deno \
--no-report \
--test docker_e2e_${{ matrix.ecosystem }}

Expand Down Expand Up @@ -411,6 +411,30 @@ jobs:
suite: e2e_scan
- os: macos-latest
suite: e2e_scan
# Safety-hardening e2e suites. The fast non-ignored ones
# (e2e_safety_lock, e2e_safety_yarn_pnp) run via the
# standard `test` job above on all three platforms, so no
# matrix entry is needed for them. The two below need real
# toolchains and are #[ignore]-gated.
- os: ubuntu-latest
suite: e2e_safety_cargo_build
- os: macos-latest
suite: e2e_safety_cargo_build
- os: windows-latest
suite: e2e_safety_cargo_build
- os: ubuntu-latest
suite: e2e_safety_pnpm
- os: macos-latest
suite: e2e_safety_pnpm
# pnpm-on-Windows uses junctions for symlinks and copies
# (not hardlinks) by default, so the CoW invariant holds
# vacuously. Test still runs to verify apply doesn't error
# on Windows — semantic Windows nlink coverage is a
# follow-up (`std::fs::Metadata` doesn't expose nlink on
# Windows; needs `GetFileInformationByHandle` via
# `windows-sys`).
- os: windows-latest
suite: e2e_safety_pnpm
runs-on: ${{ matrix.os }}
steps:
- name: Checkout
Expand All @@ -436,11 +460,20 @@ jobs:
restore-keys: ${{ matrix.os }}-cargo-e2e-

- name: Setup Node.js
if: matrix.suite == 'e2e_npm' || matrix.suite == 'e2e_scan'
if: matrix.suite == 'e2e_npm' || matrix.suite == 'e2e_scan' || matrix.suite == 'e2e_safety_pnpm'
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20.20.2'

- name: Setup pnpm
if: matrix.suite == 'e2e_safety_pnpm'
# Pin the major version so the store layout the test
# asserts on stays stable. `npm install -g` is the simplest
# cross-platform install path (works on ubuntu, macos,
# windows-runners — they all ship a usable npm via
# actions/setup-node).
run: npm install -g pnpm@10

- name: Setup Python
if: matrix.suite == 'e2e_pypi'
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
Expand Down Expand Up @@ -483,7 +516,7 @@ jobs:
strategy:
fail-fast: false
matrix:
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget]
ecosystem: [npm, pypi, gem, cargo, golang, maven, composer, nuget, deno]
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
Expand Down
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ once_cell = "=1.21.3"
qbsdiff = "=1.4.4"
tar = "=0.4.45"
flate2 = "=1.1.9"
fs2 = "=0.4.3"
wiremock = "=0.6.5"
portable-pty = "=0.9.0"
testcontainers = "=0.27.3"
Expand Down
6 changes: 6 additions & 0 deletions crates/socket-patch-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ golang = ["socket-patch-core/golang"]
maven = ["socket-patch-core/maven"]
composer = ["socket-patch-core/composer"]
nuget = ["socket-patch-core/nuget"]
deno = ["socket-patch-core/deno"]
# Enables the Docker-driven real-package e2e test suite under
# `tests/docker_e2e_*.rs`. Tests in this suite require either a running
# Docker daemon OR `SOCKET_PATCH_TEST_HOST=1` (host-toolchain mode).
Expand All @@ -49,3 +50,8 @@ base64 = { workspace = true }
reqwest = { workspace = true }
tempfile = { workspace = true }
serial_test = { workspace = true }
# Used by `tests/e2e_safety_lock.rs` to externally hold the same
# `.socket/apply.lock` the binary takes, then spawn the binary and
# assert the lock_held exit-code contract. Same crate the binary
# uses internally (`socket-patch-core::patch::apply_lock`).
fs2 = { workspace = true }
84 changes: 83 additions & 1 deletion crates/socket-patch-cli/src/commands/apply.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@ use socket_patch_core::api::blob_fetcher::{
get_missing_blobs, DownloadMode,
};
use socket_patch_core::api::client::get_api_client_with_overrides;
use socket_patch_core::crawlers::{CrawlerOptions, Ecosystem};
use socket_patch_core::crawlers::{
detect_npm_pkg_manager, CrawlerOptions, Ecosystem, NpmPkgManager,
};
use socket_patch_core::manifest::operations::read_manifest;
use socket_patch_core::patch::apply::{
apply_package_patch, verify_file_patch, ApplyResult, PatchSources, VerifyStatus,
};

use crate::commands::lock_cli::acquire_or_emit;
use socket_patch_core::utils::purl::strip_purl_qualifiers;
use socket_patch_core::utils::telemetry::{track_patch_applied, track_patch_apply_failed};
use std::collections::{HashMap, HashSet};
Expand Down Expand Up @@ -129,6 +133,11 @@ pub(crate) fn result_to_event(result: &ApplyResult, dry_run: bool) -> PatchEvent
.map(AppliedVia::from_core),
})
.collect();
// Sidecar data is NOT attached here — it's surfaced at the
// envelope level under `Envelope.sidecars[]` by the run loop.
// See `Envelope::record_sidecar`. Keeping events clean of
// sidecar info means each event describes only the apply
// action; sidecar reporting is a separate, JOIN-able list.
PatchEvent::new(PatchAction::Applied, purl).with_files(files)
}

Expand All @@ -154,6 +163,70 @@ pub async fn run(args: ApplyArgs) -> i32 {
return 0;
}

// Serialize against concurrent socket-patch runs targeting the same
// `.socket/` directory. The guard releases on function return; see
// `socket_patch_core::patch::apply_lock`.
let socket_dir = manifest_path.parent().unwrap_or(Path::new("."));
let _lock = match acquire_or_emit(
socket_dir,
Command::Apply,
args.common.json,
args.common.silent,
args.common.dry_run,
) {
Ok(guard) => guard,
Err(code) => return code,
};

// Package-manager layout detection. yarn-berry PnP keeps packages
// inside `.yarn/cache/*.zip` and resolves them via `.pnp.cjs` —
// the npm crawler can't reach them and rewriting zips is a
// different operation entirely. Refuse with a clear pointer to
// `yarn patch`. pnpm gets an informational event; the CoW guard
// in `apply_file_patch` does the substantive safety work.
let pkg_manager = detect_npm_pkg_manager(&args.common.cwd);
match pkg_manager {
NpmPkgManager::YarnBerryPnP => {
if args.common.json {
let mut env = Envelope::new(Command::Apply);
env.dry_run = args.common.dry_run;
env.mark_error(EnvelopeError::new(
"yarn_pnp_unsupported",
"yarn-berry Plug'n'Play layout is not supported by socket-patch (packages live inside .yarn/cache zips). Use `yarn patch <pkg>` instead.",
));
println!("{}", env.to_pretty_json());
} else if !args.common.silent {
eprintln!("Error: yarn-berry Plug'n'Play layout is not supported.");
eprintln!(
" Packages live inside .yarn/cache/*.zip — socket-patch cannot rewrite them in place."
);
eprintln!(" Use `yarn patch <pkg>` instead.");
}
return 1;
}
NpmPkgManager::Pnpm => {
if !args.common.json && !args.common.silent {
eprintln!(
"Note: pnpm layout detected. Copy-on-write will keep the global store untouched."
);
}
// Non-fatal — CoW handles the safety. JSON consumers see
// the layout-detected info in the apply envelope's
// existing events (no separate event added here yet).
}
NpmPkgManager::Bun => {
if !args.common.json && !args.common.silent {
eprintln!(
"Note: bun layout detected. Copy-on-write will keep ~/.bun/install/cache/ untouched."
);
}
// Same shape as pnpm: bun hard-links from its global
// install cache by default. The CoW guard handles the
// safety; this is informational only.
}
_ => {}
}

match apply_patches_inner(&args, &manifest_path).await {
Ok((success, results, unmatched)) => {
let patched_count = results
Expand All @@ -166,6 +239,13 @@ pub async fn run(args: ApplyArgs) -> i32 {
env.dry_run = args.common.dry_run;
for result in &results {
env.record(result_to_event(result, args.common.dry_run));
// Sidecar records live on the envelope, not on
// individual events. Consumers iterate
// `envelope.sidecars[]` and JOIN against
// `events[]` by `purl` for per-package context.
if let Some(ref sidecar) = result.sidecar {
env.record_sidecar(sidecar.clone());
}
}
// Manifest entries that targeted in-scope ecosystems but
// had no installed package on disk — emit one Skipped
Expand Down Expand Up @@ -705,6 +785,7 @@ mod tests {
files_patched: vec!["package/index.js".to_string()],
applied_via,
error: None,
sidecar: None,
}
}

Expand Down Expand Up @@ -779,6 +860,7 @@ mod tests {
],
applied_via,
error: None,
sidecar: None,
};

let event = result_to_event(&result, false);
Expand Down
Loading
Loading