GeezerVison is showing github.com — Fix vendored outcomes a failed commit or rolled-back eject still repo… · SocketDev/socket-patch@d73c27c
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: crates/socket-patch-cli/CLI_CONTRACT.md +4-4Lines changed: 4 additions & 4 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -148,7 +148,7 @@ For a **9.0 root lock**, the CLI ensures `pnpm-workspace.yaml` carries `trustLoc | 148 | 148 |
| 149 | 149 | **Vendored write durability (v5.0)**: every write is atomic (stage + rename), but only the durable commit points — lockfiles, `go.mod`/`go.sum`, `pom.xml`, `nuget.config`, `package.json`, `pnpm-workspace.yaml`, `.cargo/config.toml`, the Python/Ruby manifests, `.socket/vendor/state.json` and `redirect-state.json` — are fsynced on write. The content-verified artifacts under `.socket/vendor/<eco>/<uuid>/` (patched copies, packed/rebuilt archives and sidecars, markers) are written without an fsync and made durable by one barrier (file + directory fsync, one `F_FULLFSYNC` per device on macOS) ahead of the next commit point — and, for an artifact rebuilt in place that no commit point follows, at the end of the vendored run's commit and when the command releases the apply lock — so a crash can only lose an artifact that no durable commit point names yet, which the next run redownloads. | 150 | 150 |
| 151 | | -**Vendored group commit (v5.0)**: `vendor`, `scan --mode vendored` and `get --mode vendored` capture every lockfile / manifest / config edit and every ledger save of the run in memory (reads inside the run see them) and commit them ONCE after the per-package loop — including the packages that succeeded in a run where others failed, so a completed run leaves the same files per-package commits would. Captured: every file under the project root outside `.socket/`, plus `.socket/vendor/state.json` and `.socket/vendor/redirect-state.json`; artifacts are written directly (see the durability note). A multi-file commit goes through a roll-forward journal, `.socket/vendor/.commit-journal.json` (the new bytes of every changed file, plus the bytes each replaces and their sha256; deleted once the commit completes). **Crash semantics**: before the journal is durable, nothing is committed — the lockfiles and ledgers are the pre-run ones and the run's artifacts are unreferenced orphans; after it, the next command that takes the apply lock replays the journal before reading anything (files already at their new bytes are left alone), so a locked command never observes a half-committed run. A journal that matches neither side of some file (edited by hand since the crash) is renamed to `.socket/vendor/.commit-journal.set-aside-<uuid>.json` (keeping every file's pre-commit bytes) and stderr says what was done (`Warning: an interrupted vendored run's commit could not be finished as written: …`): the edited files are never written over; when they all still carry the commit's own lines the rest of the commit is finished around them, when none of them does the files the crash had already replaced are put back to their pre-commit bytes, and otherwise nothing is applied. A journal that is unreadable, names a path outside the lockfiles and ledgers, or would write through a symbolic link is set aside with nothing applied. A replay that fails on I/O keeps the journal and fails the lock acquire (`lock_io`, naming the journal). Read-only commands that take no lock (`vex`, `list`) may observe the interrupted state until then. A re-vendor under a newer uuid removes the replaced uuid's dir only after the commit (its `vendor_stale_artifact_removed` event follows the run's per-package events), and a golang takeover removes the `.socket/go-patches/` copy only after the commit that repoints `go.mod`. A commit never renames over a symbolic link: when a changed file is a symlink, the whole commit is refused before anything is written, with the top-level error `redirect_symlinked_file_unsupported` (exit 1; a `--dry-run` predicts it with a `vendor_would_refuse_symlinked_file` advisory) — the artifact dirs the run's loop added under `.socket/vendor/` are removed, and every package it vendored is reported as a `failed` event with that code instead of `applied`. A commit write failure is the top-level error `vendor_commit_failed` (exit 1; the pre-run lockfiles and ledger stay — unless putting back the files already replaced failed too, in which case the journal is kept and the next locked command finishes the commit). `repair`, `vendor --revert` and `rollback` still save per entry. | | 151 | +**Vendored group commit (v5.0)**: `vendor`, `scan --mode vendored` and `get --mode vendored` capture every lockfile / manifest / config edit and every ledger save of the run in memory (reads inside the run see them) and commit them ONCE after the per-package loop — including the packages that succeeded in a run where others failed, so a completed run leaves the same files per-package commits would. Captured: every file under the project root outside `.socket/`, plus `.socket/vendor/state.json` and `.socket/vendor/redirect-state.json`; artifacts are written directly (see the durability note). A multi-file commit goes through a roll-forward journal, `.socket/vendor/.commit-journal.json` (the new bytes of every changed file, plus the bytes each replaces and their sha256; deleted once the commit completes). **Crash semantics**: before the journal is durable, nothing is committed — the lockfiles and ledgers are the pre-run ones and the run's artifacts are unreferenced orphans; after it, the next command that takes the apply lock replays the journal before reading anything (files already at their new bytes are left alone), so a locked command never observes a half-committed run. A journal that matches neither side of some file (edited by hand since the crash) is renamed to `.socket/vendor/.commit-journal.set-aside-<uuid>.json` (keeping every file's pre-commit bytes) and stderr says what was done (`Warning: an interrupted vendored run's commit could not be finished as written: …`): the edited files are never written over; when they all still carry the commit's own lines the rest of the commit is finished around them, when none of them does the files the crash had already replaced are put back to their pre-commit bytes, and otherwise nothing is applied. A journal that is unreadable, names a path outside the lockfiles and ledgers, or would write through a symbolic link is set aside with nothing applied. A replay that fails on I/O keeps the journal and fails the lock acquire (`lock_io`, naming the journal). Read-only commands that take no lock (`vex`, `list`) may observe the interrupted state until then. A re-vendor under a newer uuid removes the replaced uuid's dir only after the commit (its `vendor_stale_artifact_removed` event follows the run's per-package events), and a golang takeover removes the `.socket/go-patches/` copy only after the commit that repoints `go.mod`. A commit never renames over a symbolic link: when a changed file is a symlink, the whole commit is refused before anything is written, with the top-level error `redirect_symlinked_file_unsupported` (exit 1; a `--dry-run` predicts it with a `vendor_would_refuse_symlinked_file` advisory) — the artifact dirs the run's loop added under `.socket/vendor/` are removed, and every package it vendored is reported as a `failed` event with that code instead of `applied` (its per-package `skipped` advisories from the same run, such as `vendor_prebuilt_downloaded`, are dropped). A commit write failure is the top-level error `vendor_commit_failed` (exit 1; the pre-run lockfiles and ledger stay, the artifact dirs the run added are removed and its vendored packages are `failed` events with that code, the same way — unless putting back the files already replaced failed too, in which case the journal is kept, the next locked command finishes the commit, and the per-package events stand). `repair`, `vendor --revert` and `rollback` still save per entry. | 152 | 152 |
| 153 | 153 | `scan --sync` is sugar for `--mode agent --prune` — the canonical single-flag agent-mode bot invocation. `scan --json --sync` discovers, applies, and reconciles state in one pass. | 154 | 154 |
| @@ -635,7 +635,7 @@ package then fails, exactly the files the eject wrote are put back (the pins' fi | 635 | 635 | restore's files, and every file the vendored apply committed, each only when its bytes changed) — | 636 | 636 | the project stays hosted exactly as before, and every other file (a `--json > report.json` redirect | 637 | 637 | target, a log another process appends to) is left alone, with the | 638 | | -`eject_rolled_back` warning (printed to stderr on a human run, which prints no "Vendored N packages" summary and no "Next steps:") and `partial_failure`, exit 1 — each package the vendored apply had vendored is re-reported as a `skipped` event with `errorCode: "eject_rolled_back"`, never `applied` (#1005); if putting the snapshot back itself fails, | | 638 | +`eject_rolled_back` warning (printed to stderr on a human run, which prints no "Vendored N packages" summary and no "Next steps:") and `partial_failure`, exit 1 — each package the vendored apply had vendored is re-reported as a `skipped` event with `errorCode: "eject_rolled_back"`, never `applied`, and the run's other `skipped` advisories for it (`vendor_prebuilt_downloaded`, `vendor_takeover_reverted_redirect`, …) are dropped (#1005); if putting the snapshot back itself fails, | 639 | 639 | the error is `eject_rollback_failed` naming the files to `git checkout`. The eject does not emit the | 640 | 640 | per-purl `vendor_takeover_reverted_redirect` warning (the restore is its own planned step). | 641 | 641 | `--offline` (or `SOCKET_OFFLINE`) refuses the eject up front with `offline_eject_unavailable` — | @@ -1235,8 +1235,8 @@ Every `--json` invocation emits a single JSON object that follows the **unified | 1235 | 1235 | | `out_of_scope_copies_restored` | rollback `warnings[]` | path-scoped rollback (v5.0): a selected patch had installed copies outside the given patterns; ALL copies were restored (patches are per-package). Informational — never flips the exit. | | 1236 | 1236 | | `vendor_ledger_entry_unwired` | scan `warnings[]` | a vendored entry's dependency left the lockfile (upgraded or removed), so the ledger supplement skipped it; the detail names the purls and points at `scan --prune`, which reverts them (no warning on a pruning non-hosted run). An entry that prune drift-keeps (its lock entries were re-resolved since vendoring, e.g. an npm uninstall re-locked it away) is reported on the prune's `GC: kept` line and keeps being warned about. | | 1237 | 1237 | | `path_scope_excluded_supplements` | scan `warnings[]` | path-scoped scan (v5.0): lockfile-only / vendor-ledger supplement packages have no installed path and were excluded from the scoped scan; the detail carries the count. | | 1238 | | -| `vendor_commit_failed` | top-level error (`vendor`, and the nested vendor envelope of `scan` / `get --mode vendored`) | v5.0 group commit: the run's lockfile / manifest / ledger edits could not be written (the detail names the I/O error). Exit 1; the project's lockfiles and `.socket/vendor/state.json` are left as they were before the run (a partially-applied commit is put back), and the per-package events describe the uncommitted outcome. When putting a partially-applied commit back fails too, the journal is kept instead and the detail says the next socket-patch command in the project finishes the commit. | | 1239 | | -| `redirect_symlinked_file_unsupported` (vendored) | top-level error (`vendor`, and the nested vendor envelope of `scan` / `get --mode vendored`) | v5.0 group commit (#627): a file the run would rewrite — a lockfile, `package.json`, `pnpm-workspace.yaml`, `nuget.config`, … — is a symbolic link. The commit stages each file and renames it over the path, which would replace the link with a detached copy and leave its target (the lock other checkouts read) unpatched, so it refuses before writing anything — the same code and message as the hosted guard. Exit 1; the link, its target and `.socket/vendor/state.json` are left as they were, the artifact dirs the run added under `.socket/vendor/` are removed (any that cannot be are named in the message, with `vendor --revert` as the remedy), and each package the run vendored is a `failed` event with this code, not `applied` (`summary.applied` does not count it). The human run prints no "Vendored N packages" count for them and no "Next steps:" (#898). Backends that check their own targets first (bun.lockb, Hatch, uv, Poetry, Pipenv, requirements, Cargo) keep their own codes. | | | 1238 | +| `vendor_commit_failed` | top-level error (`vendor`, and the nested vendor envelope of `scan` / `get --mode vendored`); also `failed` (per package) | v5.0 group commit: the run's lockfile / manifest / ledger edits could not be written (the detail names the I/O error). Exit 1; the project's lockfiles and `.socket/vendor/state.json` are left as they were before the run (a partially-applied commit is put back). As for `redirect_symlinked_file_unsupported` (#898), the artifact dirs the run added under `.socket/vendor/` are removed (any that cannot be are named in the detail), each package the run vendored is a `failed` event with this code, not `applied`, its `skipped` advisories from the run are dropped, and the human run prints no "Vendored N packages" count for them and no "Next steps:". When putting a partially-applied commit back fails too, the journal is kept instead, the detail says the next socket-patch command in the project finishes the commit, and the per-package events stand (that commit does complete). | | | 1239 | +| `redirect_symlinked_file_unsupported` (vendored) | top-level error (`vendor`, and the nested vendor envelope of `scan` / `get --mode vendored`) | v5.0 group commit (#627): a file the run would rewrite — a lockfile, `package.json`, `pnpm-workspace.yaml`, `nuget.config`, … — is a symbolic link. The commit stages each file and renames it over the path, which would replace the link with a detached copy and leave its target (the lock other checkouts read) unpatched, so it refuses before writing anything — the same code and message as the hosted guard. Exit 1; the link, its target and `.socket/vendor/state.json` are left as they were, the artifact dirs the run added under `.socket/vendor/` are removed (any that cannot be are named in the message, with `vendor --revert` as the remedy), and each package the run vendored is a `failed` event with this code, not `applied` (`summary.applied` does not count it); the `skipped` advisories the run recorded for it (`vendor_prebuilt_downloaded`, `vendor_artifact_reused`, …) are dropped, since they describe the vendoring that was refused. The human run prints no "Vendored N packages" count for them and no "Next steps:" (#898). Backends that check their own targets first (bun.lockb, Hatch, uv, Poetry, Pipenv, requirements, Cargo) keep their own codes. | | 1240 | 1240 | | `vendor_would_refuse_symlinked_file` | `skipped` (advisory event) under `vendor --dry-run`; a `warnings: [{code, detail}]` entry on the `would_vendor` / `would_revendor` row of the `vendor` preview under `scan` / `get --mode vendored --dry-run` (human: an `[warning] <purl> (<code>): <detail>` line) | dry run (#627): a dry run captures no writes, so for each package it would vendor (not one previewed as in sync, whose re-run writes nothing) it names every symlinked file of that package's ecosystem a vendored run may rewrite (the registry's vendored rewrite targets plus `pnpm-workspace.yaml`, `nuget.config`, `packages.lock.json`, the root `pom.xml`, `.mvn/maven.config` and `hatch.toml`; files a vendored run only reads, such as `.yarnrc.yml` or `vlt.json`, are never named); the wet run refuses with `redirect_symlinked_file_unsupported` if it must rewrite one. Does not change the exit code. | | 1241 | 1241 | | `vendor_state_unreadable` | rollback `warnings[]`; remove top-level error | corrupt-ledger containment (v5.0). Rollback: an unreadable vendor ledger skips the vendored leg + manifest cleanup + GC and drives `partial_failure` exit 1 while the agent and hosted legs still run. Remove: a hard top-level error before any mutation. Also the Bun vendored preflight's refusal code: `get` / `scan --mode vendored`, `vendor`'s pre-takeover check and the `--dry-run` `would_refuse` preview report an unreadable `.socket/vendor/state.json` as itself (`errorCode` in `patches[]` / `download.patches[]`, or `get <uuid>`'s top-level `error.code`), fail-closed — nothing is exempt — instead of a Bun lock code. (v4's `redirect_state_unreadable` is no longer emitted: v5 never reads the redirect ledger on these paths.) | | 1242 | 1242 | | `manifest_write_failed` | rollback `warnings[]` | rollback (v5.0): the post-rollback manifest update could not be written; no entries were removed (`manifest.removedEntries: []`) and the run exits `partial_failure` 1. | | |
Collapse file Copy file name to clipboardExpand all lines: crates/socket-patch-cli/src/commands/vendor.rs +26-10Lines changed: 26 additions & 10 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -494,10 +494,7 @@ async fn unwired_check_failure( | 494 | 494 | /// entry (fail-safe): ecosystems other than npm, cargo and pypi (whose | 495 | 495 | /// probe covers the requirements flavor only) have no in-use probe yet, | 496 | 496 | /// and a missing/unreadable lockfile proves nothing. | 497 | | -pub(crate) async fn dispatch_in_use_one( | 498 | | - entry: &VendorEntry, | 499 | | - project_root: &Path, | 500 | | -) -> Option<bool> { | | 497 | +pub(crate) async fn dispatch_in_use_one(entry: &VendorEntry, project_root: &Path) -> Option<bool> { | 501 | 498 | match entry.ecosystem.as_str() { | 502 | 499 | "npm" => vendor::npm_flavor::vendored_entry_in_use(entry, project_root).await, | 503 | 500 | // Cargo probes the lock entry's shape: detached + `[patch]` pointing | @@ -1289,8 +1286,7 @@ async fn run_check(args: &VendorArgs) -> i32 { | 1289 | 1286 | // know (the ledger was ignored or dropped from the commit along with the | 1290 | 1287 | // manifest) leaves every fresh install failing; the manifest keys above | 1291 | 1288 | // cannot see it, so the references are read from the wiring itself. | 1292 | | - let references = | 1293 | | - crate::commands::vendored_backend::repair::scan_vendor_references(root).await; | | 1289 | + let references = crate::commands::vendored_backend::repair::scan_vendor_references(root).await; | 1294 | 1290 | for (eco, uuid, rel) in references { | 1295 | 1291 | let ledgered = state | 1296 | 1292 | .entries | @@ -2770,8 +2766,9 @@ pub(crate) async fn vendor_records_reusing( | 2770 | 2766 | let mut status = StatusLine::stderr(common.json, common.silent); | 2771 | 2767 | let total = all_packages.len(); | 2772 | 2768 | // The vendored artifact dirs before this run wrote any: a commit the | 2773 | | - // symlink check refuses removes the ones the loop added, so the refusal | 2774 | | - // leaves no orphan artifact behind (#898). A dir the pre-run ledger | | 2769 | + // symlink check refuses (or that fails with nothing written) removes | | 2770 | + // the ones the loop added, so it leaves no orphan artifact behind | | 2771 | + // (#898). A dir the pre-run ledger | 2775 | 2772 | // already names (an artifact redownloaded in place) is kept: it needs | 2776 | 2773 | // no commit to be referenced. | 2777 | 2774 | let vendor_dirs_before = (!common.dry_run).then(|| VendorDirsBefore { | @@ -3544,11 +3541,30 @@ pub(crate) async fn vendor_records_reusing( | 3544 | 3541 | this project finishes it" | 3545 | 3542 | ) | 3546 | 3543 | } else { | 3547 | | - format!( | | 3544 | + // Nothing was committed: like the symlink refusal | | 3545 | + // (#898), the artifacts the loop downloaded are removed | | 3546 | + // and its packages are reported failed, not applied. | | 3547 | + let mut detail = format!( | 3548 | 3548 | "could not commit the vendored lockfile, manifest and ledger edits: \ | 3549 | 3549 | {e}; the project's lockfiles and .socket/vendor/state.json are \ | 3550 | 3550 | unchanged" | 3551 | | - ) | | 3551 | + ); | | 3552 | + let leftovers = | | 3553 | + remove_new_vendor_dirs(common, vendor_dirs_before.as_ref()).await; | | 3554 | + if !leftovers.is_empty() { | | 3555 | + detail = format!( | | 3556 | + "{detail} (except the downloaded artifacts that could not be \ | | 3557 | + removed: {}; `socket-patch vendor --revert` removes them)", | | 3558 | + leftovers.join("; ") | | 3559 | + ); | | 3560 | + } | | 3561 | + env.retract_applied( | | 3562 | + events_start, | | 3563 | + PatchAction::Failed, | | 3564 | + "vendor_commit_failed", | | 3565 | + &detail, | | 3566 | + ); | | 3567 | + detail | 3552 | 3568 | }; | 3553 | 3569 | if !common.json { | 3554 | 3570 | eprintln!("Error: {detail}"); | |
|