diff --git a/scripts/ci/materialize_base_javascript_packages.py b/scripts/ci/materialize_base_javascript_packages.py index 7defba36f..2e394ddb5 100644 --- a/scripts/ci/materialize_base_javascript_packages.py +++ b/scripts/ci/materialize_base_javascript_packages.py @@ -1,5 +1,12 @@ #!/usr/bin/env python3 -"""Materialize pnpm locks from a validated pull-request base commit.""" +"""Materialize pnpm locks from a validated pull-request base commit. + +A ``pnpm-lock.yaml`` whose sibling ``package.json`` does not pin an exact pnpm +``packageManager`` is treated as a genuine pnpm project only when no sibling +``package-lock.json`` exists; otherwise it is a vestigial second lockfile in an +npm-managed project and is skipped so the downstream npm install path handles it +instead of failing coverage evidence. +""" from __future__ import annotations @@ -97,6 +104,14 @@ def base_pnpm_projects( if not isinstance(package_manager, str) or not PNPM_SPEC_RE.fullmatch( package_manager ): + if str(project_root / "package-lock.json") in regular_paths: + # A sibling package-lock.json means npm owns this project and + # the pnpm-lock.yaml is a vestigial second lockfile. Skip pnpm + # materialization so the downstream npm (package-lock.json) + # install path handles it, instead of failing the whole + # coverage-evidence job. A genuine pnpm-only project (no sibling + # package-lock.json) still must pin an exact pnpm packageManager. + continue raise ValueError( f"trusted base package manifest {package_path} must declare an exact pnpm packageManager version" ) diff --git a/tests/test_materialize_base_javascript_packages.py b/tests/test_materialize_base_javascript_packages.py index 2534f906f..039673a67 100644 --- a/tests/test_materialize_base_javascript_packages.py +++ b/tests/test_materialize_base_javascript_packages.py @@ -208,6 +208,34 @@ def fake_git(_repo_root: Path, _command: str, object_spec: str) -> bytes: materializer.base_pnpm_projects(tmp_path, "a" * 40) +def test_skips_npm_project_with_vestigial_pnpm_lock( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """An npm project's stray pnpm-lock.yaml is skipped, not fail-closed. + + A base tree with a ``pnpm-lock.yaml`` plus a sibling ``package-lock.json`` + and no exact pnpm ``packageManager`` is npm-managed, so pnpm materialization + is skipped (the downstream npm install path owns it) rather than failing the + whole coverage-evidence job. + """ + regular_paths = { + "frontend/package.json", + "frontend/pnpm-lock.yaml", + "frontend/package-lock.json", + } + monkeypatch.setattr( + materializer, "_regular_base_paths", lambda *_args: regular_paths + ) + + def fake_git(_repo_root: Path, _command: str, object_spec: str) -> bytes: + if object_spec.endswith(":frontend/package.json"): + return b"{}" + raise AssertionError(f"unexpected git object: {object_spec}") + + monkeypatch.setattr(materializer, "_git", fake_git) + assert materializer.base_pnpm_projects(tmp_path, "a" * 40) == [] + + def test_rejects_mutable_or_non_pnpm_package_manager(tmp_path: Path) -> None: """Only an exact pnpm runner specification may populate the trusted store.""" repo, base_sha = fixture_repo(tmp_path)