Skip to content

Commit 9b7b5c2

Browse files
feat(scan): unified auto-update engine — --sync, --prune, --dry-run (v3.0) (#79)
* feat(scan): add --apply + structured updates for auto-update bot workflows Enables `socket-patch scan` as the engine for an automated "update all patches" workflow — a cron job or PR check that runs scan, detects new or updated patches against the local manifest, applies them, and either commits the change or opens a PR. Today this isn't quite possible because: * `scan --json` is read-only — it prints the discovery JSON and exits before the apply path runs, so there's no clean way to make it mutate the manifest from a bot. * Updates aren't reported in JSON — update detection (existing manifest entry with same PURL but different UUID) only runs in the non-JSON table-print path, so a `--json` consumer can't tell which patches would be updates vs net-new additions. * Per-patch JSON records lose the added-vs-updated distinction — every successful download is reported as `action: "added"` even when it's replacing an existing entry with a newer UUID. Three additive (semver-MINOR) changes resolve all of the above: 1. `commands/get.rs` — `download_and_apply_patches` now emits per-patch `{action: "updated", oldUuid}` when the PURL already had a different UUID before insert. A new pure helper `decide_patch_action(manifest, purl, new_uuid)` returns `Added | Updated{old_uuid} | Skipped` and is unit-tested independently. 2. `commands/scan.rs` — new `--apply` flag (default `false`) opts JSON callers into the full discover → select → apply pipeline. Without `--apply`, `scan --json` keeps its prior read-only contract; with it, `scan --json --apply` runs the same selection + download path the non-JSON branch uses and emits one combined JSON object with an `apply` sub-object reporting per-patch outcomes. The JSON discovery emission also now always includes a top-level `updates` array (with `purl`, `oldUuid`, `newUuid`) computed via a new pure helper `detect_updates`. `severity_order` is exposed as `pub(crate)` so it can be unit-tested. 3. `CLI_CONTRACT.md` documents the new `--apply` flag, the full `scan` discovery and `--apply` JSON shapes, and pins the per-patch action vocabulary (`added`/`updated`/`skipped`/`failed`) with semver policy clauses for adding (MINOR) or renaming/removing (MAJOR) values. ## Tests * scan.rs inline #[cfg(test)] mod tests — 4 severity_order cases + 8 detect_updates cases covering: no manifest, empty packages, no overlap, same UUID, different UUID, multiple updates, empty patch list, first-patch candidate selection. * get.rs inline test module — 4 decide_patch_action cases covering Added (no existing entry), Skipped (same UUID), Updated (different UUID with oldUuid populated), and Added-for-different-PURL (keying on PURL not UUID). * tests/cli_parse_scan.rs — `--apply` parser tests (defaults false, long form, combines with --json/--yes) + a subprocess JSON-shape test that runs the compiled binary against an empty tempdir and asserts the new `updates: []` key is present in stdout. All 416 lib tests pass, all integration tests pass, clippy clean. ## How a bot uses this ```bash socket-patch scan --json --apply --yes > scan-result.json jq '.apply.patches[] | select(.action == "updated") | {purl, oldUuid, uuid}' scan-result.json # Pipe into peter-evans/create-pull-request with a PR body summarizing the diff. ``` Exit code: 0 on full success (every selected patch added/updated/skipped), 1 if any `failed` records are present (and top-level `status` becomes `"partial_failure"`). Assisted-by: Claude Code:claude-opus-4-7 * feat(scan)!: garbage-collect on scan + hide gc subcommand (v3.0) After PR #79's --apply work, scan applied patches but didn't reconcile state. Orphan blob files accumulated and manifest entries for uninstalled packages stayed forever, forcing bots to chain `scan --apply` with `repair` themselves. This commit makes scan the single command needed for the auto-update workflow: * Default GC after every scan run that has scanned packages. Removes manifest entries for PURLs no longer in the crawl results, then sweeps orphan blob/diff/package-archive files via the existing cleanup_unused_blobs / cleanup_unused_archives helpers. * New --no-prune flag opts OUT of GC entirely. Useful when a missing package reflects a temporary uninstall the user wants to preserve. * The `gc` subcommand alias (and `repair` itself) is hidden from socket-patch --help. `socket-patch gc` still parses for backwards compat, just no longer listed. Existing scripts unaffected. * Workspace version bumped 2.1.4 → 3.0.0. scripts/version-sync.sh propagated the bump to every npm/socket-patch-* package.json and to pypi/socket-patch/pyproject.toml. ## JSON output additions In `scan --json` (read-only): new `gc` sub-object reports what *would* be pruned/reaped without mutating anything (preview mode). Fields: prunableManifestEntries, orphanBlobs, orphanDiffArchives, orphanPackageArchives, bytesReclaimable. In `scan --json --apply`: `gc` switches to mutation mode. Fields: prunedManifestEntries, removedBlobs, removedDiffArchives, removedPackageArchives, bytesFreed. With --no-prune: gc is emitted as { "skipped": true } in both modes. In the empty-crawl case (no packages found at all), gc is { "skipped": true } — pruning every manifest entry on the assumption the user "uninstalled everything" is too destructive. ## Tests * 5 new detect_prunable unit tests covering empty manifest, all present, missing entries, and full prune. * --no-prune parser tests in tests/cli_parse_scan.rs (default false, long form, combines with --apply/--json/--yes). * 4 new tests in tests/cli_parse_repair.rs locking the v3.0 deprecation: top-level --help doesn't list `repair` or `[aliases: gc]`, but `socket-patch gc` still resolves to Repair and `socket-patch repair --help` still works directly. * CleanupResult gains #[derive(Default)] so scan can build empty summaries when the cleanup helpers report errors. cargo build/clippy/test --workspace --all-features all clean. 100 lib tests in CLI (+5), 19 in cli_parse_repair (+4), 26 in cli_parse_scan (+2). 416 lib tests in core unchanged. ## Breaking changes (MAJOR bump 2.1.4 → 3.0.0) * scan --apply prunes manifest entries for uninstalled packages by default. Scripts that ran `scan --apply --yes` and relied on manifest entries surviving across an uninstall break unless they add --no-prune. * scan --apply removes orphan blob/archive files on every run (non-breaking in practice — the apply path simply re-fetches anything it needs — but a visible filesystem change). * `socket-patch gc` no longer appears in top-level --help. The subcommand still works. Assisted-by: Claude Code:claude-opus-4-7 * test(scan): add e2e_scan test suite + CI matrix entry End-to-end tests for the scan + GC pipeline that uses the real Socket API. Mirrors the structure of tests/e2e_npm.rs — every test is #[ignore] so it only runs with --ignored, matching the existing e2e gating in .github/workflows/ci.yml. Uses the minimist@1.2.2 patch fixture (CVE-2021-44906) that the other e2e tests already share. ## Coverage (9 scenarios) * test_scan_apply_json_adds_new_patch — fresh install, `scan --json --apply --yes` reports action: "added" and patches the file on disk. * test_scan_apply_json_skips_existing — re-run shows action: "skipped". * test_scan_apply_json_updates_existing — seed manifest with a fake UUID, re-run shows action: "updated" with oldUuid populated. * test_scan_json_read_only_emits_updates_array — read-only mode surfaces the manifest-vs-API drift in the `updates` array. * test_scan_json_read_only_no_mutation — `scan --json` never creates a manifest or modifies files. * test_scan_apply_prunes_uninstalled_package_by_default — uninstall minimist, re-scan, manifest entry is gone + blobs are reaped. * test_scan_apply_no_prune_keeps_uninstalled_entries — same scenario with --no-prune leaves manifest + blobs intact, gc reports { skipped: true }. * test_scan_apply_cleans_orphan_blobs — plant a stray orphan blob, next scan run removes it and reports gc.removedBlobs >= 1. * test_scan_json_read_only_gc_preview — preview mode lists prunableManifestEntries and counts orphanBlobs without mutating. ## CI integration * Added e2e_scan to the e2e job matrix on ubuntu-latest and macos-latest (mirrors how e2e_npm is matrixed). * Setup Node.js step's `if:` predicate extended to also run for e2e_scan — the suite shells out to `npm install` for fixture setup. Each #[ignore] test self-skips with a SKIP message if `npm` is not on PATH, so a future runner without npm doesn't fail spuriously. Assisted-by: Claude Code:claude-opus-4-7 * docs(scan): document v3.0 GC behavior + repair deprecation CLI_CONTRACT.md changes: * Add --no-prune row to the scan flag table with a description of the v3.0 GC default. * Extend the scan JSON output shape with the new `gc` sub-object. Document the split between preview-mode field names (prunable*/orphan*/bytesReclaimable) and apply-mode field names (pruned*/removed*/bytesFreed). Document that --no-prune emits gc: { skipped: true } in both modes. * Mark `repair` as "(deprecated since v3.0)" at the section heading. Spell out the demotion: `hide = true` on the Repair variant and `alias = "gc"` (was `visible_alias`). Removing repair or unhiding it would be a MAJOR bump. * Add semver-policy row: "Change `scan`'s default behavior (e.g. pruning, GC, apply) — MAJOR." Notes the v3.0 flip is the one grandfathered instance; future flips also MAJOR. README.md changes: * Remove the `repair`/`gc` section from the public command list (still documented in CLI_CONTRACT.md for advanced users). * Expand the `scan` section: add --apply and --no-prune flags, -y/--yes, --download-mode rows. New "Bot mode" example with `scan --json --apply --yes`. Add "Apply without pruning" example. Brief note about scan being the single command for the auto-update workflow. Assisted-by: Claude Code:claude-opus-4-7 * fix(scan): auto-select in JSON --apply when multiple free patches exist The free-tier patch API may serve multiple free patches for the same PURL (e.g., minimist@1.2.2 currently has 2 free patches). The `scan --json --apply` path was calling `select_patches(... is_json = true)` which returns `Err(JsonModeNeedsExplicit)` with `status: "selection_required"` in that scenario — no forward progress, the bot can't apply anything. For scan-driven workflows there's no "specify --id" option (we're scanning the whole project), so the right behavior is to auto-select the newest patch and continue. Pass `is_json = false` so the non-TTY branch inside `select_one` auto-selects index 0 — which is the most-recently-published patch (the group is sorted by `published_at` descending before `select_one` runs). Also relaxed the e2e_scan test assertions so they don't pin a specific upstream UUID/hash: * added/updated/skipped tests assert action vocabulary, PURL match, and "file was patched" (not exact AFTER_HASH). * updated test asserts the new UUID differs from the seeded oldUuid rather than matching a hardcoded constant. * read-only updates test similarly asserts `newUuid != oldUuid`. These changes make the e2e suite robust to API churn — the contract is "an apply happened", not "this specific patch was selected". Assisted-by: Claude Code:claude-opus-4-7 * fix(scan): run apply-mode GC even when no packages have patches The post-uninstall scenario hit an edge case: after a user uninstalls the only patched package and installs a new (unpatched) one, the next `scan --apply --yes` would crawl successfully, find no packages with patches, and skip the entire `--apply` block. The read-only preview GC ran instead, emitting `gc.prunableManifestEntries` (preview field name) but never actually pruning anything from the manifest. A bot relying on `scan --apply` to reach a clean state would loop forever — the stale manifest entry never gets removed. Fix: when `--apply` is set but no packages have patches, still run the mutating GC pass and emit an empty `apply` sub-object plus the `gc.prunedManifestEntries` (apply field name). Bots can now trust `scan --apply --yes` to converge to a clean state in one pass even when the crawl has no patched packages. Also dropped the now-unused `NPM_UUID` and `AFTER_HASH` constants from the e2e_scan test file (warning noise from relaxing the assertions in the previous commit). Assisted-by: Claude Code:claude-opus-4-7 * feat(scan): pivot GC to opt-in via --prune + add --sync and --dry-run Reverses the v3.0 GC-by-default decision. After feedback, default-on GC was too aggressive: scripts running `scan --apply --yes` against a project with a temporarily-uninstalled package would silently destroy the manifest entry, breaking dev workflows where the package gets reinstalled later. The new opt-in model: * `--prune` (new, default false) opts into garbage collection. Manifest entries for packages no longer in the crawl are removed, then `cleanup_unused_blobs` + `cleanup_unused_archives` sweep orphan files. Without `--prune`, scan leaves `.socket/` alone. * `--sync` (new) is sugar for `--apply --prune`. The canonical bot invocation becomes `scan --json --sync --yes` (3 flags; `--json` and `--yes` are workflow scaffolding). * `--dry-run` / `-d` (new) previews what `--apply`/`--prune`/`--sync` would do without mutating disk. The `apply.patches[*]` array is populated via `decide_patch_action`, and `gc.prunable*` / `gc.orphan*` field names are emitted (instead of `pruned*` / `removed*`). The `apply.dryRun: true` flag explicitly marks the output for bots that need a single signal. * `--no-prune` field removed (it was the inverse of the now-default behavior). ## Implementation * `ScanArgs.no_prune` → `ScanArgs.prune` (semantics inverted). New `sync` and `dry_run` fields. * At the top of `scan::run`, `let apply = args.apply || args.sync;` and `let prune = args.prune || args.sync;` — derive once, use everywhere downstream. `--sync` is purely additive sugar. * `run_apply_gc` no longer takes a `no_prune: bool` parameter — callers always gate on `prune` before calling it. When GC isn't requested, the `gc` JSON field is omitted entirely (no `{ "skipped": true }` placeholder). * New `preview_apply_gc` helper for the dry-run path. Runs `cleanup_unused_blobs` / `cleanup_unused_archives` with `dry_run=true` and emits preview field names via `GcSummary::to_preview_json`. * Dry-run apply path synthesizes per-patch `apply.patches[]` records via `super::get::decide_patch_action` against the on-disk manifest — accurately reports added/updated/skipped for the selected patches without actually calling `download_and_apply_patches`. * Empty-cwd JSON branch drops `gc: { skipped: true }` (no `gc` field at all when GC wasn't requested). Drive-by fix: `tests/ecosystem_dispatch::partition_purls_allow_list_excludes_one` now uses `!map.contains_key(&Ecosystem::Pypi)` instead of the `unnecessary_get_then_check` lint trigger. Assisted-by: Claude Code:claude-opus-4-7 * revert(repair): restore gc as a documented subcommand The prior v3.0 iteration demoted `Commands::Repair`'s `gc` alias to a hidden `alias = "gc"` and added `hide = true` on the subcommand itself, banking on `scan` becoming the all-in-one command for both apply and GC. With GC now opt-in via `--prune`/`--sync` (see prior commit), `repair`/`gc` is the right answer for users who want to clean up without an apply pass. Restore `#[command(visible_alias = "gc")]` and drop `hide = true` so the subcommand appears in `socket-patch --help` again with its `[aliases: gc]` hint. Update the four hidden-help tests in `tests/cli_parse_repair.rs`: * `repair_is_hidden_from_top_level_help` → `repair_appears_in_top_level_help` (assertion inverted). * `gc_alias_is_hidden_from_top_level_help` → `gc_alias_is_visible_in_top_level_help` (assertion inverted). * `gc_alias_still_parses_for_backwards_compat` → `gc_alias_parses_as_repair` (simplified — alias is no longer deprecated, so the "backwards compat" framing is gone). * `repair_subcommand_help_still_works_directly` dropped (was a deprecation-era assertion). These tests now lock the *opposite* contract: removing or hiding the `gc` alias is a MAJOR bump. Assisted-by: Claude Code:claude-opus-4-7 * test(scan): update parser + e2e tests for opt-in GC cli_parse_scan.rs: * defaults_match_contract now asserts !args.prune, !args.sync, !args.dry_run (replacing the old !args.no_prune line). * no_prune_flag_long_form → prune_flag_long_form; assertion inverted (passing --prune sets prune=true). * no_prune_combines_with_apply_and_json → prune_combines_with_apply_and_json. * NEW: sync_flag_long_form — --sync sets sync=true; does NOT auto-derive --apply/--prune at parse time (that derivation happens inside scan::run). * NEW: sync_combines_with_json_and_yes. * NEW: dry_run_long_form (--dry-run sets dry_run=true). * NEW: dry_run_short_form (-d sets dry_run=true). e2e_scan.rs: * Module docstring updated to describe opt-in GC. * test_scan_apply_prunes_uninstalled_package_by_default → test_scan_apply_prune_prunes_uninstalled_package — now passes --prune explicitly. * test_scan_apply_no_prune_keeps_uninstalled_entries → test_scan_apply_default_keeps_uninstalled_entries — drops the --no-prune flag (it no longer exists); asserts the gc field is omitted entirely. * test_scan_apply_cleans_orphan_blobs → test_scan_apply_prune_cleans_orphan_blobs — passes --prune. * test_scan_json_read_only_gc_preview split into: - test_scan_dry_run_sync_previews_apply_and_gc — exercises the new --dry-run flag combined with --sync; verifies preview output is populated AND nothing on disk changed. - test_scan_json_no_gc_field_without_prune — locks the contract that `gc` is omitted when --prune isn't set. * NEW: test_scan_sync_yes_full_lifecycle — installs minimist, runs --sync (adds patch), uninstalls + plants orphan, runs --sync again (prunes + sweeps). End-to-end exercise of the canonical bot mode. Total e2e_scan scenarios: 11 (was 9). Assisted-by: Claude Code:claude-opus-4-7 * docs(scan): document --prune/--sync/--dry-run + un-deprecate gc CLI_CONTRACT.md: * Scan flag table: replace --no-prune row with three new rows — --prune, --sync, -d/--dry-run. Add a paragraph explaining each plus the canonical bot-mode invocation. * JSON output shape: drop the --no-prune-emits-{skipped:true} note. Clarify that `gc` is omitted ENTIRELY when --prune/--sync isn't set. Document --dry-run behavior including the explicit `apply.dryRun: true` marker for bots. * New "scan — --sync (bot mode)" section with the canonical `scan --json --sync --yes | jq '{applied, pruned, bytes_freed}'` recipe. * New "scan — --dry-run" section explaining that --dry-run is a no-op without one of the mutating flags. * Restore the `repair` section's normal heading (drop the "*(deprecated since v3.0)*" suffix and the deprecation paragraph). Note that the `gc` visible_alias is now contract-guarded. * Semver-policy table: drop the GC-default row, add explicit rows for "flipping --prune to opt-out" and "demoting `gc` from visible_alias" — both MAJOR. README.md: * Restore the `### repair` / `gc` section that was removed during the deprecation iteration. Wording clarifies that `repair`/`gc` is the right answer for cleanup-without-apply and points users at `scan --sync` for the combined workflow. * `### scan` section: replace --no-prune row with --prune, --sync, --dry-run, --yes. Bot-mode example becomes `scan --json --sync --yes` (the user's "one or two flags" target). Add a `scan --json --sync --yes --dry-run` example. * `## Scripting & CI/CD`: lead with the new `--sync` recipe piped through jq into `peter-evans/create-pull-request`. Keep the old `scan --json --ecosystems npm` read-only example as the second use case. Assisted-by: Claude Code:claude-opus-4-7 * chore(hardening): pin toolchain, deps, actions, and install bootstrap - rust-toolchain.toml: pin channel to exact version with components - Cargo.toml: exact-pin all workspace dependencies via =X.Y.Z spec - crates/{cli,core}/Cargo.toml: wire dev-deps through workspace pins - npm/socket-patch/package.json: exact-pin runtime + dev deps; commit package-lock.json so downstream installs are deterministic - scripts/install.sh: download SHA256SUMS, verify tarball digest before extraction; accept SOCKET_PATCH_VERSION env override - scripts/version-sync.sh: preserve the leading = on exact-pin specs; refresh the npm lockfile on every version bump - .github/workflows/release.yml: SHA-pin actions, pin npm@version, pin language toolchain versions for setup-* actions - .github/workflows/pin-check.yml: new fail-closed workflow that greps every uses: line and rejects non-SHA-pinned action refs * feat(cli)!: non-mutating apply + unified JSON envelope (v3.0) Two related changes that together complete the v3.0 contract: * apply no longer writes to .socket/. When the manifest is missing blobs in offline mode, apply bails with a partial_failure envelope. When online and missing blobs need fetching, the bytes go to an OS tempdir overlay for the duration of the run; .socket/ stays read- only. Garbage collection moves out of apply entirely (now lives in scan --prune / repair / gc). * New crates/socket-patch-cli/src/json_envelope.rs defines a shared Envelope/PatchEvent/Status/Summary shape that every --json invocation now emits. The action vocabulary (added/updated/skipped/ applied/downloaded/removed/failed/verified) is the single contract downstream consumers route on. CLI_CONTRACT.md is updated with the unified shape + jq recipes. Migrated commands: apply, list, repair, remove. (scan, get, rollback, setup retain their pre-v3.0 shapes for now and are documented as pending in CLI_CONTRACT.md.) Also removes three dead-code items the audit confirmed have zero callers: - crates/socket-patch-core/src/utils/enumerate.rs (whole module) - crates/socket-patch-core/src/utils/global_packages.rs (whole module; npm crawler ships the live copy) - path_to_group_id() in maven_crawler (test-only inverse helper) - false-positive allow(dead_code) on get.rs::DownloadParams BREAKING: every migrated subcommand's --json output is reshaped to the new envelope (camelCase status, events array, summary block). * test: comprehensive in-process + subprocess test suite Adds ~120 new tests across the apply/scan/get/list/remove/repair/ rollback/setup CLI commands. Tests drive socket-patch in-process (commands::*::run) and via subprocess against wiremock-backed API fixtures, asserting on disk state, JSON envelope shape, and exit codes. Includes: * apply: invariants test (no .socket/ mutation), network tests with wiremock, edge cases (read-only files, nested dirs, multi-file, hash mismatch, idempotent re-apply, missing files, force overrides) * scan: invariants, sync end-to-end, dry-run preview, --apply + --prune combinations * get: identifier-type detection (UUID/CVE/GHSA/PURL/package), --save-only path, paid_required path, error paths, edge cases * repair: download-mode variants (file/diff/package), offline mode, blob cleanup verification * remove: PURL + UUID identifiers, rollback chain, blob cleanup * rollback: real bytes restore for all 8 ecosystems via handcrafted fixtures + real installer paths * setup: package.json detection, pnpm monorepo handling, dry-run * PTY-driven interactive prompt tests (portable-pty) * Alternate installer configs: yarn, pnpm, npm workspaces, bundler * Python venv variants: 3.11/3.12/3.13, .env/venv/.venv layouts, VIRTUAL_ENV override, canonical name normalization, egg-info legacy Real package managers are used where available on host (npm, pip, gem, cargo); ecosystems without host toolchains (go/maven/composer/ nuget) use handcrafted fixtures that exactly mirror what their native installers produce on disk. * ci: add coverage job, language version pins, and e2e-docker matrix * New 'coverage' job: cargo-llvm-cov (LLVM source-based instrumentation via taiki-e/install-action), uploads lcov.info as a workflow artifact and prints the summary to the GitHub Actions job summary. Report-only (no --fail-under threshold) so contributors get visibility without flaky CI when coverage shifts. * Language toolchain pins on every setup-* action invocation: Node 20.20.2, Python 3.12.13, Ruby 3.2.11. dtolnay/rust-toolchain now reads from rust-toolchain.toml (the toolchain: stable input is dropped from every step). * New 'e2e-docker' matrix: ubuntu-latest x { npm, pypi, gem, cargo, golang, maven, composer, nuget }. Each slot builds the shared base image and the per-ecosystem layer via docker/build-push-action with scope-cached layers (type=gha,scope=test-<eco>), then runs the corresponding 'cargo test --features docker-e2e --test docker_e2e_<eco>'. Triggered on every PR. The existing 'e2e' job (real Socket API, --ignored) stays for nightly/manual real-API smoke runs. * test(docker-e2e): infrastructure + npm full install→apply chain Adds the Docker-driven e2e test infrastructure: * tests/docker/Dockerfile.base: multi-stage build (rust:1.93-slim- bookworm builder → debian:12-slim runtime + compiled socket-patch). Base layer shared by every ecosystem image. Both base images pinned by sha256 digest. * tests/docker/Dockerfile.npm: FROM base + Node 20 LTS via NodeSource. * tests/docker/README.md: how to build images locally, run tests with Docker or with SOCKET_PATCH_TEST_HOST=1 host mode, and how to add a new ecosystem. * tests/docker/fixtures/npm/README.md: documents the synthetic fixture approach (--force apply against any installed bytes). * docker_e2e_npm.rs: real 'npm install minimist@1.2.2' inside the container, wiremock served patch, scan --sync writes manifest + blob, apply --force overwrites the on-disk file, then grep-verifies SOCKET-PATCH-E2E-MARKER in node_modules/minimist/index.js. Hermetic (no Socket API contact); reproducible in CI. This is the working template every other ecosystem extends. * test(e2e): full install→apply chain for pypi in Docker (+ global) Upgrades docker_e2e_pypi.rs from scan-discovery-only to the full chain, twice: once for local (venv) install and once for global (pip --break-system-packages). * Switches the fixture package from pydantic-ai (heavy transitive deps, ~60s install) to six 1.16.0 (single-file, ~1s install). * pypi's file-path convention has NO `package/` prefix — the python crawler returns site-packages root as pkg_path, so the patch's file path is just `six.py` (lands at site-packages/six.py). * `pypi_local_install_full_apply_chain`: venv install at .venv/lib/ python3.X/site-packages/six.py, scan --sync writes manifest + blob, apply --force --offline overwrites the file. Grep verifies SOCKET-PATCH-E2E-MARKER on disk. * `pypi_global_install_full_apply_chain`: pip install --break- system-packages installs into Debian's system Python site- packages. scan + apply with --global. Same marker verification at the system-site-packages path discovered via `python3 -c "import six; print(six.__file__)"`. The Dockerfile.pypi already has python3 + pip + venv from prior infrastructure work; no Dockerfile change. * test(e2e): full install→apply chain for gem in Docker (+ global) Two tests for the Ruby ecosystem: * gem_local_install_full_apply_chain: `gem install --install-dir vendor/bundle/ruby/<ver> colorize -v 1.1.0` produces the bundle- style layout that the Ruby crawler scans in local mode. scan --sync + apply --force overwrites lib/colorize.rb with the synthetic patched content; marker verified on disk. * gem_global_install_full_apply_chain: plain `gem install colorize -v 1.1.0` (no --install-dir) installs to `$(gem env gemdir)`. The --global flag drives the Ruby crawler to scan the system gem dir. Same marker check at the discovered path. gem patches use the `package/<rel>` convention; apply strips the `package/` prefix and joins with the gem's directory. Dockerfile.gem is unchanged from prior infrastructure work. * test(e2e): full install→apply chain for cargo in Docker `cargo fetch` against a minimal project with `cfg-if = "=1.0.0"` populates `\$CARGO_HOME/registry/src/<index>/cfg-if-1.0.0/`. scan --sync writes the manifest + blob, apply --force --offline overwrites the registry-source `src/lib.rs` with patched bytes containing SOCKET-PATCH-E2E-MARKER. grep verifies on disk. Pre-chmods the registry source file to writable — cargo's source files are read-only by default and apply's own fix-permissions code covers the same path, but the chmod up-front keeps the test robust against changes there. Single test (no global variant): cargo's registry is the only cache, so local-vs-global is a no-op. Dockerfile.cargo unchanged from prior infrastructure work; it has rustup-installed Rust 1.93.1 with CARGO_HOME set. * test(e2e): full install→apply chain for golang in Docker `go mod download github.com/gin-gonic/gin@v1.9.1` populates `\$GOMODCACHE/github.com/gin-gonic/gin@v1.9.1/`. scan --sync writes the manifest + blob, apply --force --offline overwrites gin.go with synthetic patched bytes containing SOCKET-PATCH-E2E-MARKER. grep verifies on disk. Pre-chmods the cache file to writable — `go mod download` extracts to read-only files, similar to cargo registry. Single test (no global variant): golang's module cache is the only cache; --global is a no-op. Dockerfile.golang ships Go 1.21.13 from the official tarball; GOPATH and GOMODCACHE are set at image build time. * test(e2e): full install→apply chain for maven in Docker Upgrades docker_e2e_maven.rs from scan-only to the full chain. `mvn dependency:get -Dartifact=org.apache.commons:commons-lang3:3.12.0` downloads the artifact into ~/.m2/repository, the wiremock fixture overwrites the .pom file with synthetic patched bytes, and the test grep-verifies SOCKET-PATCH-E2E-MARKER on disk. Single test (local-only) since ~/.m2 is always global. * test(e2e): full install→apply chain for composer in Docker Upgrades docker_e2e_composer.rs to the full chain plus a global variant. Real `composer require monolog/monolog:3.5.0` installs into vendor/monolog/monolog/, the wiremock fixture overwrites src/Monolog/Logger.php with synthetic patched bytes, and the test grep-verifies SOCKET-PATCH-E2E-MARKER on disk. Adds composer_global_install_full_apply_chain: `composer global require` installs to $COMPOSER_HOME/vendor, socket-patch scans + applies with --global, marker verified there. * test(e2e): full install→apply chain for nuget in Docker Upgrades docker_e2e_nuget.rs to the full chain plus a global variant. The local test redirects `dotnet add package` to a project-local ./packages dir via NUGET_PACKAGES, then scan + apply patch the package's LICENSE.md with a synthetic blob; the global test uses the default ~/.nuget/packages and --global mode. Note: the wiremock fixture uses the lowercased package name in the PURL ("newtonsoft.json") so scan's GC pass (--sync = --apply --prune) doesn't prune the freshly-saved manifest entry — the crawler reports installed packages by their lowercased directory name and GC keys against that. * test(e2e): add npm global install variant in Docker Adds npm_global_install_full_apply_chain alongside the existing local install/apply/rollback test. The variant runs `npm install -g`, locates the file at $(npm root -g)/minimist/index.js, then runs scan + apply with --global and grep-verifies SOCKET-PATCH-E2E-MARKER. Host-mode skips the global variant (no safe host npm prefix to mutate); Docker is the canonical run path. * test(e2e): add composer/maven/nuget Dockerfiles Adds the three Dockerfile recipes the docker_e2e_{composer,maven,nuget} tests panic-message instruct users to build. - Dockerfile.composer: base + PHP 8 + Composer 2 - Dockerfile.maven: base + default-jdk-headless + maven - Dockerfile.nuget: mcr.microsoft.com/dotnet/sdk:8.0 (sdk image) with socket-patch COPY'd in from the base * ci(coverage): include docker-e2e in the coverage map The host `coverage` job ran with `--all-features`, which enabled the docker-e2e feature, but the job never built the per-ecosystem Docker images — every docker_e2e_<eco> test would panic on `assert_image` and the job failed (or, if it ever passed, only the surviving in-process tests contributed). The Docker tests exercise the real socket-patch binary inside a Linux container, and that subprocess's coverage wasn't captured at all. Changes: * Each `docker_e2e_<eco>.rs` now reads SOCKET_PATCH_COV_BIN + SOCKET_PATCH_COV_PROFRAW_DIR. When both are set, the docker run mounts an llvm-cov-instrumented socket-patch binary over the image's baked-in /usr/local/bin/socket-patch and points LLVM_PROFILE_FILE into a host-visible volume. Empty Vec when unset → tests behave exactly as before for local dev and the existing e2e-docker matrix. * `coverage` job: drops `--all-features` for an explicit feature list (cargo,golang,maven,composer,nuget) that excludes docker-e2e. Produces `coverage-host.lcov`. * New `coverage-docker` matrix job: per ecosystem, builds the base + ecosystem Docker images, eval-sources `cargo llvm-cov show-env` to build an instrumented `target/debug/socket-patch`, sets the SOCKET_PATCH_COV_* hooks, runs `cargo llvm-cov --no-report --test docker_e2e_<eco>`, and emits a per-ecosystem lcov artifact. * New `coverage-merge` job: gathers `coverage-host` + all 8 `coverage-docker-*` artifacts and unions them via `lcov --add-tracefile` into a single `coverage-lcov` artifact. Same artifact name as before so downstream consumers keep working. Result: lines hit by ANY test (host in-process, host harness, or in-container binary execution) show up in the final coverage map. * ci: remove cargo cache from docker-image-building jobs zizmor's cache-poisoning audit (high) flagged the cargo `actions/cache` steps in `e2e-docker` and `coverage-docker` because both jobs also invoke `docker/build-push-action`. The risk model: a PR could poison the cargo cache (target/, ~/.cargo) with a backdoored crate or compiled object, and a later run on a trusted ref could load the poisoned cache and produce a compromised binary that gets mounted into the docker container or baked into the published image. Drop the cargo cache from both jobs. The Docker buildx `cache-from: type=gha` remains, so image-layer rebuilds are still fast. Cargo deps refresh from the registry per run — about a one-minute cost that's worth it to eliminate the attack surface. The other jobs (clippy, test, test-release, coverage, e2e) keep their cargo caches — none of them build Docker images, so the audit doesn't trigger for them. * ci: provide explicit toolchain input to dtolnay/rust-toolchain The action requires a `toolchain` input — when SHA-pinned (which is our policy), the action can't infer the channel from action_ref the way `@stable`/`@1.93.1` ref pins would, so it errors out with "'toolchain' is a required input". The original comments ("toolchain version is read from rust-toolchain.toml") referred to rustup's behavior after install, not the action's pre-install resolution — the action doesn't read rust-toolchain.toml itself. Set `toolchain: "1.93.1"` on every Install Rust step, matching the channel in rust-toolchain.toml. The duplication is intentional: if they drift, rustup will reconcile by installing the toolchain.toml channel on first cargo invocation, just at a small extra cost. * ci: drop dtolnay/rust-toolchain in favor of inline rustup Removes the third-party Rust toolchain action and replaces every "Install Rust" step with `rustup show`. rustup is pre-installed on GitHub-hosted runners; `rustup show` consumes rust-toolchain.toml, auto-installs the pinned channel if missing, and applies the listed components (rustfmt, clippy). For coverage jobs that additionally need llvm-tools-preview, `rustup component add llvm-tools-preview` follows the show step. Benefits: - One less third-party action to audit and SHA-pin. - No duplication between rust-toolchain.toml and ci.yml. - Toolchain bumps are one-file changes (just edit toolchain.toml). * refactor(cli): unify CLI args + env-var bindings across every subcommand Define a single `GlobalArgs` clap struct and `#[command(flatten)]` it into every subcommand's args. Every flag now has a matching `SOCKET_*` env var binding (precedence: CLI > env > default). Legacy `SOCKET_PATCH_PROXY_URL`, `SOCKET_PATCH_DEBUG`, `SOCKET_PATCH_TELEMETRY_DISABLED` are still honored at runtime via a one-shot deprecation warning that fires even under `--silent` / `--json`. Behavior changes: - `--offline` now means strict airgap on every command (was three different things across apply / repair / rollback). On `repair`, `--offline` and `--download-only` are mutually exclusive. - `repair --download-mode` default flipped from `file` to `diff` to match every other command. Users who need the legacy per-file blob behavior opt in with `--download-mode file`. - `apply` and `repair` gain `--api-url` / `--api-token` / `--org` for free via the flatten (previously only readable via env). - `--debug` and `--no-telemetry` promoted from env-only toggles to CLI flags. CLI_CONTRACT.md rewritten around a single global-args table plus a small per-subcommand section for local flags. New tests: `cli_global_args.rs` (compose test: every global flag × every subcommand) and `cli_env_deprecation.rs` (legacy-env warning fires under `--silent` / `--json`). Assisted-by: Claude Code:opus-4-7 * docs(changelog): add CHANGELOG.md + CI guard blocking publish without entry Adds a Keep-a-Changelog-style CHANGELOG.md at the repo root, backfilled with concise summaries for every published tag (v1.1.0 → v2.1.4) and a detailed v3.0.0 entry covering the breaking changes in the in-flight v3 release (unified `--offline`, `repair --download-mode` default flip, `SOCKET_PATCH_*` → `SOCKET_*` env-var renames with one-shot deprecation warning, shared `GlobalArgs` flatten across every subcommand, etc.). Wires a new step into the `Release` workflow's `version` job that fails the workflow when `CHANGELOG.md` lacks an entry for the version in Cargo.toml. Because every downstream job (tag, build, github-release, cargo/npm/pypi-publish) transitively depends on `version`, a missing changelog entry blocks the entire publish pipeline. Accepts both `## [X.Y.Z]` and `## X.Y.Z` heading styles to keep the format requirement loose for future contributors. Assisted-by: Claude Code:opus-4-7 * ci(test): unbreak test/coverage/e2e-docker matrix on feat/scan-apply-json This commit fixes the pre-existing CI red on the v3.0 branch. Four unrelated root causes: 1. `cargo test --workspace --all-features` enables the `docker-e2e` feature, which compiles the 8 `docker_e2e_<eco>.rs` tests on every `test (ubuntu/macos/windows)` runner. Those tests `assert_image()` on a docker image that only exists in the dedicated docker-building jobs, so every test runner failed. Replaced each `assert_image()` panic with a `skip_if_no_image()` early return that prints a stderr skip notice. Tests now report `ok` on hosts without docker / images. `cargo test --workspace --all-features` is green everywhere. 2. The `coverage` job (cargo-llvm-cov, --all-features) failed three `in_process_remove_repair_lifecycle` tests that set `SOCKET_API_URL`/`SOCKET_API_TOKEN`/`SOCKET_ORG_SLUG` via `std::env::set_var` after constructing `RepairArgs` via `..GlobalArgs::default()`. The refactor's `api_client_overrides()` was always forwarding the resolved api_url/proxy_url as `Some(...)`, which short-circuited the env-var fallback inside `get_api_client_with_overrides`. Made `GlobalArgs::default()` leave `api_url`/`proxy_url` empty (clap always populates them in production via `default_value`, so the production path is unchanged) and `api_client_overrides()` filters empty values to `None`. The env-var fallback now fires for these tests. 3. `repair_download_only_skips_cleanup` (in `repair_invariants.rs`) used the shared `run_repair()` helper which injects `--offline`. v3.0 made `--offline` and `--download-only` mutually exclusive (exit code 2). Inlined the binary invocation without `--offline` for this one test — the manifest's referenced blob is already on disk so the download phase is a no-op even without `--offline`. 4. The `e2e-docker` and `coverage-docker` matrix jobs failed at "Build <eco> image" with `pull access denied` on `socket-patch-test-base:latest`. setup-buildx-action defaults to the `docker-container` driver, which runs BuildKit in a sandboxed container that cannot see the host docker daemon's image store — so the per-ecosystem Dockerfile's `FROM socket-patch-test-base:latest` tries to pull from docker.io and fails. Switched both jobs to `driver: docker` so buildx talks to the host daemon directly. Dropped the `type=gha` cache directives (not supported under the docker driver) — we trade build cache for image visibility. Local: `cargo test --workspace --all-features` → 965 passed, 0 failed. Assisted-by: Claude Code:opus-4-7 * ci(coverage-docker): pin to ubuntu-22.04 for glibc compatibility The coverage-docker matrix builds an instrumented socket-patch binary on the host and mounts it into the debian:12-slim test container. ubuntu-latest is currently 24.04 (glibc 2.39); debian:12 ships glibc 2.36. Result: every coverage-docker matrix job failed with socket-patch: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.39' not found (required by socket-patch) Pin to ubuntu-22.04 (glibc 2.35) — the highest base that's forward-compatible with debian:12. e2e-docker is unaffected because it runs the binary that was baked into the image by the base Dockerfile's internal builder stage, not a host-mounted one. Assisted-by: Claude Code:opus-4-7 * test(npm): connect via 127.0.0.1 in infrastructure smoke (Windows fix) wiremock binds to 0.0.0.0 (the wildcard). Linux and macOS quietly route a connect to 0.0.0.0 onto the loopback interface, so the test worked on those runners. Windows refuses the connect with WSAEADDRNOTAVAIL (winsock error 10049) because 0.0.0.0 is a valid bind target but not a valid destination address. Use 127.0.0.1 explicitly for the smoke-check URL — the bound port from `server.address().port()` is still what we need. Assisted-by: Claude Code:opus-4-7 * test(pypi): skip in_process_pypi_apply on Windows (Unix venv layout) `install_six()` hardcodes `venv/bin/pip` and `find_site_packages()` walks `venv/lib/pythonX.Y/site-packages/`. Both layouts are Unix-only — on Windows the venv puts pip at `Scripts\pip.exe` and site-packages at `Lib\site-packages\` (no per-version subdirectory). Rather than forking the helpers per platform, gate every test in this file behind `skip_unsupported_platform()` which prints a skip notice on Windows and returns early. The same code paths get exercised by the Linux test runner and the docker_e2e_pypi suite, so coverage isn't lost. Assisted-by: Claude Code:opus-4-7 * test(pypi): make in_process_pypi_apply helpers platform-aware (Windows) Reverts the earlier Windows-skip in favor of real Windows coverage. Three changes to the helpers: 1. find_python() probes `python3` → `python` → `py` (mirrors the crawler's `find_python_command` in core/src/crawlers/python_crawler.rs:15). On Windows the canonical name is `python` (the `py` launcher is also installed); `python3` is rare. Without this the venv-creation step calls `python3` and fails on every Windows runner. 2. venv_pip() returns `Scripts\pip.exe` on Windows vs `bin/pip` on Unix, matching PEP-405's documented venv layout. 3. find_site_packages() branches on cfg!(windows): * Windows: `<venv>\Lib\site-packages\` — no version subdirectory. * Unix: glob `<venv>/lib/python3.X/site-packages/` for whatever interpreter version pip used. The four in-process tests now exercise the same install→scan→apply chain on Windows that they already cover on Linux/macOS. The core crawler is already Windows-aware (python_crawler.rs:182) so the package-discovery path it tests is real, not synthetic. Assisted-by: Claude Code:opus-4-7 * test(rollback): use Windows venv layout when cfg!(windows) rollback_pypi_restores_original_content set up a synthetic `.venv/lib/python3.11/site-packages/` tree by hand. That's the Unix layout — on Windows the pypi crawler at core/src/crawlers/python_crawler.rs:182 looks for `.venv\Lib\site-packages\`, so on Windows runners the crawler found nothing and the patched file was never rolled back. Branch on `cfg!(windows)` when building the path so the synthetic package sits where the crawler actually probes on each platform. The crawler logic itself is unchanged. Assisted-by: Claude Code:opus-4-7 * feat(json): enrich added/updated patch records with description, severity, vuln IDs When `get` or `scan --apply` adds or updates a patch in the manifest, the per-patch JSON record now carries the metadata consumers need to render the patch to a human without a second API round-trip: ```jsonc { "purl": "pkg:npm/minimist@1.2.2", "uuid": "11111111-...", "action": "added", "description": "Fixes prototype pollution in minimist", "license": "MIT", "tier": "free", "exportedAt": "2024-01-01T00:00:00Z", "severity": "high", "vulnerabilities": [ { "id": "GHSA-xvch-5gv4-984h", "cves": ["CVE-2024-12345"], "severity": "high", "summary": "Prototype Pollution", "description": "merge() does not check Object.prototype" } ] } ``` Highlights: - Top-level `severity` is the max across the vulnerabilities array, using the ordering critical > high > medium=moderate > low. - `vulnerabilities[]` is sorted by `id` so consumer diffs and test snapshots don't flap on HashMap iteration order. - Metadata is intentionally omitted on `action: skipped` (consumer already has it from the original add) and on `action: failed`. - `scan --apply` benefits automatically — both flows go through `download_and_apply_patches`. Helpers `severity_rank`, `max_vuln_severity`, `patch_event_metadata` are pub(crate) and unit-tested. CLI_CONTRACT.md gains a new "`patches[]` entry shape" subsection documenting the schema. Assisted-by: Claude Code:opus-4-7 * test(e2e): walk the v3.0 envelope for `list --json` output The e2e (real-registry) suite was asserting on `list["patches"]` — the pre-v3 ad-hoc shape. v3.0 migrated `list --json` to the unified envelope, which emits `{command, status, events, summary}` with one `discovered` event per manifest entry. Patch metadata (vulnerabilities, tier, license) lives under `details`. Updated four sites (e2e_npm × 2, e2e_pypi × 1, e2e_gem × 1) to filter events by `action == "discovered"` and walk `details.vulnerabilities[]` for CVE assertions. Closes the `e2e (ubuntu/macos, e2e_npm|e2e_pypi|e2e_gem)` matrix failures surfaced once the e2e workflow started passing on the v3.0 branch. Assisted-by: Claude Code:opus-4-7 * ci(e2e_gem): bump pinned ruby from 3.2.11 to 3.2.10 `ruby/setup-ruby` dropped 3.2.11 from its catalog at some point — the action errors with "Unknown version 3.2.11 for ruby on ubuntu-24.04" and lists 3.2.10 as the newest 3.2.x available. 3.2.x is API-stable so 3.2.10 is a drop-in replacement. Assisted-by: Claude Code:opus-4-7 * refactor(cli): drop -d/-m short aliases; loosen version pins Two unrelated changes in one commit: 1. Drop the `-d` short for `--dry-run` and `-m` short for `--manifest-path` from `GlobalArgs`. We want those letters free for future flags. The long forms are unaffected, and a new `reserved_short_forms_are_not_assigned` compose test locks in that no subcommand reassigns either letter. Per-subcommand short-form tests (`*_short`, `manifest_path_short_form`, etc.) are deleted; the long-form counterparts cover the contract. 2. Loosen `python-version` and `ruby-version` pins in ci.yml from exact patch (`3.12.13`, `3.2.10`) to minor.x (`3.12.x`, `3.2.x`). setup-python and setup-ruby's catalogs keep retiring older patch versions and breaking the workflow — minor.x auto-resolves to whatever patch is currently available. CLI_CONTRACT.md updated to remove `-d`/`-m` from the global args table and the env-var cross-reference. Assisted-by: Claude Code:opus-4-7 * feat(apply): preserve mode + ownership across patches `apply_file_patch` now treats target-file permissions as a strict round-trip: 1. **Existing file**: snapshot mode + uid + gid before writing. - If read-only, temporarily grant owner-write so the overwrite succeeds (Go module cache, npm linked symlinks, etc.). - After writing, restore the *exact* pre-patch mode (idempotent `set_permissions(from_mode(...))`) and chown back to the pre-patch uid/gid. `tokio::fs::write` truncates + rewrites the file in place, so owner usually survives, but pinning ownership explicitly stops a theoretical race where another process opens the file between truncate and write. 2. **New file** (created by the patch): chown to inherit owner/group from the parent directory, mode = `0o444` (read-only for all). Matches how a freshly-unpacked package tarball treats its files. Windows: no uid/gid concept; preserve the readonly attribute for existing files and force it on new ones. `restore_file_permissions` and the `chown_blocking` helper are split out of `apply_file_patch` for readability and unit testing. Four new tests pin the policy: readonly-mode preservation, executable (0o755) mode preservation, new-file default mode + parent ownership inheritance, and uid/gid round-trip on existing files. Assisted-by: Claude Code:opus-4-7 * ci(e2e_gem): revert ruby-version to exact 3.2.10 setup-ruby (unlike setup-python) does NOT support the `3.2.x` wildcard pin — it errors with "Unknown version 3.2.x for ruby on ubuntu-24.04". Revert to an exact patch that's in the catalog. When this patch eventually drops off, bump it manually per the list at https://github.com/ruby/setup-ruby. Assisted-by: Claude Code:opus-4-7
1 parent b96a13f commit 9b7b5c2

111 files changed

Lines changed: 19945 additions & 1903 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/ci.yml‎

Lines changed: 354 additions & 19 deletions
Large diffs are not rendered by default.

‎.github/workflows/pin-check.yml‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
name: Pin check
2+
3+
# Fail-closed lint that prevents unpinned action references from sneaking back
4+
# into CI. Every `uses:` entry must reference a 40-character commit SHA (not a
5+
# tag, branch, or @latest). The repo's hardening policy is to consume third-
6+
# party actions only by immutable digest.
7+
8+
on:
9+
pull_request:
10+
paths:
11+
- '.github/workflows/**'
12+
- '.github/actions/**'
13+
push:
14+
branches:
15+
- main
16+
paths:
17+
- '.github/workflows/**'
18+
- '.github/actions/**'
19+
20+
permissions: {}
21+
22+
jobs:
23+
check:
24+
runs-on: ubuntu-latest
25+
permissions:
26+
contents: read
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
30+
with:
31+
persist-credentials: false
32+
33+
- name: Verify all `uses:` references are SHA-pinned
34+
run: |
35+
set -eu
36+
# Match any `uses:` line that does NOT reference @<40-char-hex>.
37+
# Allowlist:
38+
# - local actions referenced by `uses: ./.github/actions/foo`
39+
# - `uses: docker://image@sha256:<digest>`
40+
violations="$(
41+
grep -rEn '^\s*uses:\s*' .github/workflows .github/actions 2>/dev/null \
42+
| grep -vE 'uses:\s*\./' \
43+
| grep -vE 'uses:\s*docker://[^[:space:]]+@sha256:[0-9a-f]{64}' \
44+
| grep -vE 'uses:\s*[^@[:space:]]+@[0-9a-f]{40}([[:space:]]|$|#)' \
45+
|| true
46+
)"
47+
if [ -n "$violations" ]; then
48+
echo "::error::Unpinned action references found. Pin to a 40-char commit SHA."
49+
echo "$violations"
50+
exit 1
51+
fi
52+
echo "All action references are SHA-pinned."

‎.github/workflows/release.yml‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ jobs:
3636
exit 1
3737
fi
3838
39+
- name: Check CHANGELOG.md has entry for version
40+
run: |
41+
VERSION="${{ steps.read.outputs.VERSION }}"
42+
if [ ! -f CHANGELOG.md ]; then
43+
echo "::error::CHANGELOG.md does not exist at the repository root."
44+
exit 1
45+
fi
46+
# Accept either `## [X.Y.Z]` or `## X.Y.Z` headings, with an
47+
# optional trailing space (followed by `— DATE`) or end-of-line.
48+
if ! grep -qE "^## \[?${VERSION}\]?( |$)" CHANGELOG.md; then
49+
echo "::error::CHANGELOG.md is missing an entry for version ${VERSION}."
50+
echo "::error::Add a heading like \`## [${VERSION}] — $(date +%Y-%m-%d)\` describing the release before re-running."
51+
exit 1
52+
fi
53+
3954
tag:
4055
needs: version
4156
if: ${{ !inputs.dry-run }}
@@ -126,12 +141,12 @@ jobs:
126141
- name: Install Rust
127142
uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable
128143
with:
129-
toolchain: stable
144+
# toolchain version is read from rust-toolchain.toml (exact-pinned).
130145
targets: ${{ matrix.target }}
131146

132147
- name: Install cross
133148
if: matrix.build-tool == 'cross'
134-
run: cargo install cross --git https://github.com/cross-rs/cross
149+
run: cargo install --locked --version =0.2.5 cross
135150

136151
- name: Build (cargo)
137152
if: matrix.build-tool == 'cargo'
@@ -181,6 +196,14 @@ jobs:
181196
path: artifacts
182197
merge-multiple: true
183198

199+
- name: Generate SHA256SUMS
200+
run: |
201+
cd artifacts
202+
# Hash every release artifact (tar.gz + zip) so install.sh can verify
203+
# the binary before extraction. Sorted output keeps the file stable.
204+
sha256sum *.tar.gz *.zip 2>/dev/null | sort > SHA256SUMS
205+
cat SHA256SUMS
206+
184207
- name: Create GitHub Release
185208
env:
186209
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -206,8 +229,7 @@ jobs:
206229

207230
- name: Install Rust
208231
uses: dtolnay/rust-toolchain@efa25f7f19611383d5b0ccf2d1c8914531636bf9 # stable
209-
with:
210-
toolchain: stable
232+
# toolchain version is read from rust-toolchain.toml (exact-pinned).
211233

212234
- name: Authenticate with crates.io
213235
id: crates-io-auth
@@ -258,7 +280,7 @@ jobs:
258280
registry-url: 'https://registry.npmjs.org'
259281

260282
- name: Update npm for trusted publishing
261-
run: npm install -g npm@latest
283+
run: npm install -g npm@11.15.0
262284

263285
- name: Stage binaries into platform packages
264286
run: |
@@ -341,7 +363,7 @@ jobs:
341363
- name: Setup Python
342364
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
343365
with:
344-
python-version: '3.12'
366+
python-version: '3.12.13'
345367

346368
- name: Copy README for PyPI package
347369
run: cp README.md pypi/socket-patch/README.md

‎CHANGELOG.md‎

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# Changelog
2+
3+
All notable changes to socket-patch are documented here.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
Pre-v3.0 entries are concise summaries derived from each tag's commit
9+
history. For full per-release detail, see the
10+
[GitHub releases page](https://github.com/SocketDev/socket-patch/releases).
11+
12+
The `Release` workflow refuses to publish a version that does not appear
13+
in this file — see `.github/workflows/release.yml` (`version` job).
14+
15+
## [Unreleased]
16+
17+
## [3.0.0] — 2026-05-22
18+
19+
### Breaking
20+
21+
- **`--offline` semantics unified** to strict airgap on every subcommand.
22+
Previously meant three different things across `apply` (strict airgap),
23+
`repair` (skip downloads / cleanup-only), and `rollback` (fail when blobs
24+
missing). All three now mean the same thing: never contact the network,
25+
fail loudly when a required local source is missing.
26+
- **`repair --download-mode` default** changed from `file` to `diff` to
27+
match every other subcommand. Users who need the legacy per-file blob
28+
behavior must now opt in with `--download-mode file`.
29+
- **`repair --offline` is mutually exclusive with `--download-only`** —
30+
passing both exits with code 2.
31+
- **Env vars renamed.** The three remaining `SOCKET_PATCH_*` env vars now
32+
use the `SOCKET_*` prefix:
33+
- `SOCKET_PATCH_PROXY_URL` → `SOCKET_PROXY_URL`
34+
- `SOCKET_PATCH_DEBUG` → `SOCKET_DEBUG`
35+
- `SOCKET_PATCH_TELEMETRY_DISABLED` → `SOCKET_TELEMETRY_DISABLED`
36+
37+
The legacy names are still honored at runtime but emit a one-shot
38+
deprecation warning to stderr (the warning fires even under `--silent`
39+
and `--json` because the transition signal must reach scripts and CI
40+
logs). Legacy names will be removed in v4.
41+
42+
### Added
43+
44+
- Shared `GlobalArgs` clap struct `#[command(flatten)]`-ed into every
45+
subcommand. Every flag is now accepted on every subcommand (silently
46+
no-op'd where the subcommand doesn't consume it). Every flag has a
47+
matching `SOCKET_*` env-var binding with precedence
48+
`CLI arg > env var > default`. See `CLI_CONTRACT.md` for the full
49+
global-arguments table.
50+
- `apply` and `repair` accept `--api-url`, `--api-token`, `--org` via the
51+
global flatten (previously env-var only — telemetry would silently fall
52+
back to the public proxy when the CLI was the only way to set these).
53+
- New global flags `--debug` and `--no-telemetry`, promoted from env-only
54+
toggles.
55+
- `--proxy-url` (env: `SOCKET_PROXY_URL`) as an explicit CLI knob for the
56+
public patch proxy.
57+
- New CI guard in the `Release` workflow: the workflow fails before tag
58+
creation if `CHANGELOG.md` lacks an entry for the version in
59+
`Cargo.toml`. Blocks every downstream publish (cargo, npm, pypi).
60+
61+
### Changed
62+
63+
- Garbage collection moved out of `apply`. Use `scan --prune`,
64+
`scan --sync`, or `repair` / `gc` instead. `apply` is now strictly
65+
non-mutating against `.socket/`: when blobs need to be fetched they go
66+
to a temp overlay; the persistent cache is never written to.
67+
- Unified JSON envelope (`command` / `status` / `events` / `summary`) for
68+
`apply`, `list`, `remove`, `repair`. Other subcommands keep their
69+
pre-v3 ad-hoc shapes for now; see `CLI_CONTRACT.md` for migration status.
70+
71+
## [2.1.4] — 2026-04-09
72+
73+
- Release workflow tolerates already-published npm packages so a partial
74+
publish can be retried without re-tagging.
75+
76+
## [2.1.3] — 2026-04-08
77+
78+
- Pin Node `22.22.1` in the release workflow to dodge a broken
79+
upstream npm.
80+
81+
## [2.1.2] — 2026-04-08
82+
83+
- Harden core error handling, blob verification, and `--force` reporting.
84+
- Surface `find_by_purls` errors instead of silently swallowing them.
85+
- Add diagnostics to `apply` for silent no-op failures in CI.
86+
- Add explicit Node typings for TypeScript 6 compatibility in the npm
87+
wrapper.
88+
89+
## [2.1.1] — 2026-04-02
90+
91+
- Simplify release to `workflow_dispatch` only (no bot commits).
92+
- Split release into PR-based version prep + auto-publish on dispatch.
93+
- Prioritize `pnpm-workspace.yaml` detection and restrict `setup` to root
94+
`package.json` for pnpm monorepos.
95+
- Harden GitHub Actions workflows per `zizmor` audit.
96+
- Unflag Ruby gem (`gem`) support and add e2e bundler tests.
97+
- Use `npx @socketsecurity/socket-patch` for the generated postinstall
98+
command.
99+
100+
## [2.1.0] — 2026-03-10
101+
102+
- Full glibc/musl support across all Linux architectures (16 platform
103+
combinations now published per release).
104+
105+
## [2.0.0] — 2026-03-06
106+
107+
- Interactive prompts and smart patch selection when multiple patches
108+
match a query.
109+
110+
## [1.7.1] — 2026-03-06
111+
112+
- Ensure the binary has execute permission in the PyPI wrapper.
113+
- Restore `bin` and `optionalDependencies` to the npm wrapper
114+
`package.json`.
115+
116+
## [1.7.0] — 2026-03-06
117+
118+
- Expand ecosystem support: rough-in for composer, go, maven, nuget, ruby.
119+
- Add a TypeScript schema library to the npm wrapper.
120+
- Treat empty `SOCKET_API_TOKEN` as unset.
121+
122+
## [1.6.3] — 2026-03-05
123+
124+
- Maintenance release.
125+
126+
## [1.6.2] — 2026-03-05
127+
128+
- Maintenance release (version sync).
129+
130+
## [1.6.1] — 2026-03-05
131+
132+
- Switch to per-platform `optionalDependencies` for the npm package.
133+
- Add macOS global-package crawling fallbacks and pyenv support.
134+
135+
## [1.6.0] — 2026-03-04
136+
137+
- Add support for more platforms; fix pypi and npm publish flows.
138+
139+
## [1.5.0] — 2026-03-04
140+
141+
- Fix trusted publishing setup for npm and PyPI.
142+
143+
## [1.4.0] — 2026-03-04
144+
145+
- Update PyPI publish action and add npm provenance permissions.
146+
147+
## [1.3.1] — 2026-03-04
148+
149+
- Fix action image references in the publish workflow.
150+
151+
## [1.3.0] — 2026-03-04
152+
153+
- Add `apply --force`; rename `--no-apply` to `--save-only` (the old name
154+
remains as a hidden alias).
155+
- Cargo/Rust crate patching support behind a feature flag.
156+
- Auto-resolve org slug from API token when `SOCKET_ORG_SLUG` is unset.
157+
158+
## [1.2.0] — 2026-01-10
159+
160+
- Fix publish workflow to checkout the bumped version.
161+
162+
## [1.1.0] — 2026-01-10
163+
164+
- Pin GitHub Actions to full commit SHAs and wire up version-bump
165+
support in the publish workflow.

0 commit comments

Comments
 (0)