Skip to content

Commit 3e79ecb

Browse files
mikolalysenkoclaude
andcommitted
Merge origin/main into agent/fix-hosted-unreadable-candidate
Brings in the vex_consumed alias test fix (#849) that main's red test/test-release/coverage jobs were waiting on. Co-Authored-By: Claude <noreply@anthropic.com>
2 parents 6c2c306 + 99f61d2 commit 3e79ecb

109 files changed

Lines changed: 10968 additions & 906 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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,9 @@ jobs:
10171017
- {os: ubuntu-latest, suite: mode_migration_bun, bun: '1.2.23', test_filter: --include-ignored}
10181018
- {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.0.36', test_filter: --include-ignored}
10191019
- {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.1.45', test_filter: --include-ignored}
1020+
# Bun >= 1.4 migrating a hosted workspace bun.lockb to bun.lock
1021+
# (#803): the only binary-lock test whose reader must be 1.4+.
1022+
- {os: ubuntu-latest, suite: e2e_bun_lockb, bun: '1.4.2', test_filter: --include-ignored workspace_text_migration_heals_on_rerun}
10201023
# Real-vlt capstones (DESIGN §8.4): wiremock patch service and a local
10211024
# npm registry fed from npmjs, driven by the pinned vlt release
10221025
# (`node vlt.js`, installed below from a sha512-checked `npm pack`).

‎AGENTS.md‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Agent instructions
2+
3+
## CHANGELOG.md is written only at release time
4+
5+
Don't add, edit or delete entries in `CHANGELOG.md` in a feature, fix,
6+
refactor, CI or docs PR, even if a doc, template or reviewer asks for one.
7+
The `[Unreleased]` section is written when a release is cut, by the release
8+
agent, from the PRs merged since the last tag and the code itself. Put the
9+
details of a change in its commit messages and PR description instead.
10+
11+
If a PR you work on already changes `CHANGELOG.md`, restore that file to its
12+
merge-base version. The only exceptions are release PRs: a `release/v*`
13+
branch, or the release train's `release-sync` PR.

‎CHANGELOG.md‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ Pre-v3.0 entries are concise summaries derived from each tag's commit
99
history. For full per-release detail, see the
1010
[GitHub releases page](https://github.com/SocketDev/socket-patch/releases).
1111

12-
Add entries under `[Unreleased]`; its `###` headings set the next version's
13-
bump (Breaking/Removed → major, Added/Changed/Deprecated → minor, anything
14-
else → patch). Releases are cut by the release train
12+
PRs never edit this file. Only the release agent writes `[Unreleased]`, at
13+
release time, from the PRs merged since the last tag and the code they
14+
changed. Its `###` headings set the next version's bump (Breaking/Removed →
15+
major, Added/Changed/Deprecated → minor, anything else → patch). Releases
16+
are cut by the release train
1517
([docs/release-train/DESIGN.md](docs/release-train/DESIGN.md)) with
1618
`scripts/release.py`: a release candidate's `[Unreleased]` entries become a
1719
`## [X.Y.Z-rc.N]` section, the rolling `release-sync` PR brings each cut

‎CLAUDE.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
@AGENTS.md

‎crates/socket-patch-cli/CLI_CONTRACT.md‎

Lines changed: 13 additions & 6 deletions
Large diffs are not rendered by default.

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -898,6 +898,32 @@ pub(crate) async fn run_redirect_selected(
898898
}
899899
}
900900
}
901+
// A yarn berry pin takes its `bin:` map from the served tarball's own
902+
// package.json, the way yarn builds a tarball entry (#718). Only the
903+
// berry entries that carry a `bin:` map need it; a tarball that cannot
904+
// be fetched or read drops its patch rather than pin an entry yarn
905+
// would rewrite on the next install.
906+
for dep in engine::yarn_berry_manifest_targets(&candidates, &read.files) {
907+
status.set(format!(
908+
"Fetching hosted package manifest for {}...",
909+
dep.name
910+
));
911+
match socket_patch_core::hosted::npm_manifest::fetch_hosted_npm_manifest(
912+
api_client,
913+
&dep.artifact_url,
914+
dep.integrity.sha512.as_deref(),
915+
)
916+
.await
917+
{
918+
Ok(manifest) => {
919+
python_metadata.insert(dep.artifact_url.clone(), manifest);
920+
}
921+
Err(detail) => {
922+
unavailable_python_artifacts.insert(dep.artifact_url.clone());
923+
skipped.push(engine::npm_manifest_unavailable(dep, &detail));
924+
}
925+
}
926+
}
901927
status.finish();
902928
candidates.retain(|c| !unavailable_python_artifacts.contains(&c.dep.artifact_url));
903929
// The Pipfile.lock reference shape depends on the installing Pipenv
@@ -1152,6 +1178,11 @@ pub(crate) async fn run_redirect_selected(
11521178
common,
11531179
&confirmed,
11541180
&rewrite.confirmed_pipenv_uuids,
1181+
rewrite
1182+
.files
1183+
.get("Pipfile.lock")
1184+
.or_else(|| done.files.get("Pipfile.lock"))
1185+
.map(String::as_str),
11551186
&records,
11561187
)
11571188
.await
@@ -2236,6 +2267,9 @@ fn describe_skip_reason(reason: &str) -> String {
22362267
"its vendored state could not be reverted (see the warning)".into()
22372268
}
22382269
"python_metadata_unavailable" => "the hosted wheel's metadata could not be fetched".into(),
2270+
"npm_manifest_unavailable" => {
2271+
"the hosted tarball's package.json could not be fetched".into()
2272+
}
22392273
"redirect_bun_lock_unsupported" | "redirect_bun_lockb_invalid" => {
22402274
"the Bun lockfile blocks the vendored-to-hosted migration (see the warning)".into()
22412275
}

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

Lines changed: 73 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,16 @@ pub(super) async fn stale_install_warnings(
1818
common: &crate::args::GlobalArgs,
1919
confirmed: &[(String, String)],
2020
pipenv_uuids: &BTreeSet<String>,
21+
// The run's final Pipfile.lock text: the remedy's `pipenv sync`
22+
// arguments follow the categories that pin each package.
23+
pipenv_lock: Option<&str>,
2124
// This run's fetched records MERGED with the ledger's persisted ones
2225
// (the caller hands the post-merge ledger map), looked up by uuid.
2326
records: &BTreeMap<String, PatchRecord>,
2427
) -> StaleInstallOutcome {
2528
let mut out = StaleInstallOutcome::default();
29+
let pipenv_lock: Option<serde_json::Value> =
30+
pipenv_lock.and_then(|text| serde_json::from_str(text.trim_start_matches('\u{feff}')).ok());
2631
let candidates: Vec<_> = confirmed
2732
.iter()
2833
.filter(|(purl, _)| purl.starts_with("pkg:pypi/"))
@@ -111,15 +116,15 @@ pub(super) async fn stale_install_warnings(
111116
.and_then(|rest| rest.split('@').next())
112117
.unwrap_or("<package>")
113118
.to_string();
119+
let remedy = socket_patch_core::vendor::pypi_pipenv::stale_install_remedy(
120+
pipenv_lock.as_ref(),
121+
&name,
122+
);
114123
format!(
115124
"Pipenv does not reinstall a release that is already present (`pipenv \
116125
install`, `pipenv install --deploy` and `pipenv sync` all keep those \
117126
bytes), so the rewritten Pipfile.lock only protects fresh installs. \
118-
Reinstall it from the lock without touching the Pipfile: `pipenv run pip \
119-
uninstall -y {name} && pipenv sync` (`pipenv install --deploy` before \
120-
Pipenv 2018), or `pipenv --rm && pipenv sync` for a clean virtualenv — \
121-
NOT `pipenv uninstall`, which rewrites the Pipfile and re-locks the \
122-
patch away; then `socket-patch vex --product <purl>` re-verifies the \
127+
{remedy}; then `socket-patch vex --product <purl>` re-verifies the \
123128
installed files."
124129
)
125130
} else {
@@ -194,7 +199,8 @@ mod tests {
194199
("one".into(), record("first-uuid", "first.py", b"patched")),
195200
("two".into(), record("second-uuid", "second.py", b"patched")),
196201
]);
197-
let out = stale_install_warnings(&common, &confirmed, &BTreeSet::new(), &ledger).await;
202+
let out =
203+
stale_install_warnings(&common, &confirmed, &BTreeSet::new(), None, &ledger).await;
198204
assert_eq!(out.stale_purls, BTreeSet::from([first.to_string()]));
199205
assert_eq!(out.warnings.len(), 1);
200206
assert!(out.warnings[0]["detail"]
@@ -209,7 +215,8 @@ mod tests {
209215
"variant".into(),
210216
));
211217
ledger.insert("three".into(), record("variant", "first.py", b"upstream"));
212-
let out = stale_install_warnings(&common, &confirmed, &BTreeSet::new(), &ledger).await;
218+
let out =
219+
stale_install_warnings(&common, &confirmed, &BTreeSet::new(), None, &ledger).await;
213220
assert!(out.stale_purls.is_empty());
214221
assert!(out.warnings.is_empty());
215222
}
@@ -233,8 +240,66 @@ mod tests {
233240
let purl = "pkg:pypi/six@1.16.0";
234241
let confirmed = vec![(purl.to_string(), "six-uuid".to_string())];
235242
let ledger = BTreeMap::from([("k".into(), record("six-uuid", "six.py", b"patched"))]);
236-
let out = stale_install_warnings(&common, &confirmed, &BTreeSet::new(), &ledger).await;
243+
let out =
244+
stale_install_warnings(&common, &confirmed, &BTreeSet::new(), None, &ledger).await;
237245
assert_eq!(out.stale_purls, BTreeSet::from([purl.to_string()]));
238246
assert_eq!(out.warnings[0]["code"], "redirect_pypi_stale_install");
239247
}
248+
249+
/// #790: the Pipenv remedy names the lock category that pins the
250+
/// package. Plain `pipenv sync` installs only `default`, so for a
251+
/// `[dev-packages]` entry it uninstalled the package and left it
252+
/// uninstalled.
253+
#[tokio::test]
254+
async fn pipenv_remedy_resyncs_the_category_that_pins_the_package() {
255+
let tmp = tempfile::tempdir().unwrap();
256+
let site = tmp.path().join("site-packages");
257+
std::fs::create_dir_all(site.join("six-1.16.0.dist-info")).unwrap();
258+
std::fs::write(site.join("six.py"), b"upstream").unwrap();
259+
let common = crate::args::GlobalArgs {
260+
cwd: tmp.path().to_path_buf(),
261+
global_prefix: Some(site.clone()),
262+
..Default::default()
263+
};
264+
let purl = "pkg:pypi/six@1.16.0";
265+
let confirmed = vec![(purl.to_string(), "six-uuid".to_string())];
266+
let ledger = BTreeMap::from([("k".into(), record("six-uuid", "six.py", b"patched"))]);
267+
let pipenv = BTreeSet::from(["six-uuid".to_string()]);
268+
let detail = |lock: &str| {
269+
let lock = lock.to_string();
270+
let (common, confirmed, ledger, pipenv) = (&common, &confirmed, &ledger, &pipenv);
271+
async move {
272+
let out =
273+
stale_install_warnings(common, confirmed, pipenv, Some(&lock), ledger).await;
274+
assert_eq!(out.warnings.len(), 1);
275+
out.warnings[0]["detail"].as_str().unwrap().to_string()
276+
}
277+
};
278+
279+
let develop = detail(
280+
r#"{"_meta": {"pipfile-spec": 6}, "default": {}, "develop": {"six": {"file": "x"}}}"#,
281+
)
282+
.await;
283+
assert!(
284+
develop.contains("`pipenv run pip uninstall -y six && pipenv sync --dev`"),
285+
"{develop}"
286+
);
287+
assert!(
288+
develop.contains("`pipenv --rm && pipenv sync --dev`"),
289+
"{develop}"
290+
);
291+
292+
let docs = detail(
293+
r#"{"_meta": {"pipfile-spec": 6}, "default": {"requests": {}}, "docs": {"six": {}}}"#,
294+
)
295+
.await;
296+
assert!(
297+
docs.contains("-y six && pipenv sync --categories \"docs\"`"),
298+
"{docs}"
299+
);
300+
assert!(
301+
docs.contains("`pipenv --rm && pipenv sync --categories \"packages docs\"`"),
302+
"{docs}"
303+
);
304+
}
240305
}

0 commit comments

Comments
 (0)