From 10864f0b1b66b7d36cf6f14d1ef72ab8c3f1217f Mon Sep 17 00:00:00 2001 From: Amit Turgeman Date: Thu, 8 Oct 2026 12:32:20 +0300 Subject: [PATCH] CM-74472: Collect pnpm-workspace.yaml for SCA scans A pnpm workspace declares its members in pnpm-workspace.yaml, and the backend uses that file to tie a member to the root pnpm-lock.yaml. The CLI collected pnpm-lock.yaml and every package.json but not pnpm-workspace.yaml, so in a CLI scan a member with no lockfile of its own could not be connected to the root lockfile: its dependencies were reported at their raw ranges with no transitive tree, and findings were missed. Repository scans see the file and were unaffected. Since the CLI stopped generating a lockfile inside workspace members that a root lockfile already covers, every pnpm member reaches the backend without a lockfile, which made the gap visible. The file is added to the SCA supported files, so path scans upload it, and to the npm project files, so a diff scan that touches a root project file brings it along. With it uploaded, the backend resolves pnpm members exactly as it does for repository scans. Co-Authored-By: Claude Opus 5.5 (1M context) --- cycode/cli/consts.py | 2 ++ .../sca/test_sca_file_collector.py | 21 +++++++++++++++++++ .../cli/files_collector/test_file_excluder.py | 20 ++++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/cycode/cli/consts.py b/cycode/cli/consts.py index 278c9596..48f3763b 100644 --- a/cycode/cli/consts.py +++ b/cycode/cli/consts.py @@ -124,6 +124,7 @@ def _detect_operating_system() -> OperatingSystem: 'deno.lock', 'deno.json', 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', 'bun.lock', 'npm-shrinkwrap.json', 'packages.config', @@ -189,6 +190,7 @@ def _detect_operating_system() -> OperatingSystem: 'npm-shrinkwrap.json', '.npmrc', 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', 'bun.lock', 'deno.lock', 'deno.json', diff --git a/tests/cli/files_collector/sca/test_sca_file_collector.py b/tests/cli/files_collector/sca/test_sca_file_collector.py index 13688f0a..dc2a9334 100644 --- a/tests/cli/files_collector/sca/test_sca_file_collector.py +++ b/tests/cli/files_collector/sca/test_sca_file_collector.py @@ -10,6 +10,8 @@ from cycode.cli.files_collector.sca.npm import workspace from cycode.cli.files_collector.sca.sca_file_collector import ( _add_dependencies_tree_documents, + _get_doc_ecosystem_related_project_files, + _get_project_file_ecosystem, _try_restore_dependencies, ) from cycode.cli.models import Document @@ -106,3 +108,22 @@ def test_each_scan_starts_with_a_cleared_npm_workspace_cache(self, tmp_path: Pat _add_dependencies_tree_documents(ctx, [Document(str(manifest), manifest.read_text())]) assert workspace.find_covering_workspace(str(member_dir)) is not None + + +class TestPnpmWorkspaceRelatedProjectFiles: + """A diff scan uploads the changed project file plus the project files beside it.""" + + def test_pnpm_workspace_file_is_an_npm_project_file(self) -> None: + assert _get_project_file_ecosystem(Document('repo/pnpm-workspace.yaml', '')) == 'npm' + + def test_changed_pnpm_lockfile_brings_the_workspace_file_along(self, tmp_path: Path) -> None: + """Without pnpm-workspace.yaml the backend cannot tell which directories the root lockfile resolves.""" + (tmp_path / 'package.json').write_text('{"name": "root"}') + (tmp_path / 'pnpm-workspace.yaml').write_text('packages:\n - "packages/*"\n') + lock_file = tmp_path / 'pnpm-lock.yaml' + lock_file.write_text("lockfileVersion: '9.0'\n") + + changed = Document(str(lock_file), lock_file.read_text()) + related = _get_doc_ecosystem_related_project_files(changed, [changed], 'npm', None, None) + + assert str(tmp_path / 'pnpm-workspace.yaml') in [document.path for document in related] diff --git a/tests/cli/files_collector/test_file_excluder.py b/tests/cli/files_collector/test_file_excluder.py index 31a52f55..e1d8571d 100644 --- a/tests/cli/files_collector/test_file_excluder.py +++ b/tests/cli/files_collector/test_file_excluder.py @@ -128,3 +128,23 @@ def test_parametrized_excluded_directories(self, excluded_dir: str) -> None: # File with excluded directory name in filename should be included included_path = f'project/src/{excluded_dir}_config.txt' assert _is_file_relevant_for_sca_scan(included_path) is True + + +class TestScaSupportedFiles: + """Test which file names an SCA scan collects.""" + + @pytest.mark.parametrize( + 'file_path', + ['pnpm-workspace.yaml', 'repo/pnpm-workspace.yaml', '/home/user/repo/pnpm-workspace.yaml'], + ) + def test_pnpm_workspace_file_is_collected(self, file_path: str) -> None: + """pnpm declares its workspace members in pnpm-workspace.yaml, which the backend needs to resolve them.""" + excluder = Excluder() + + assert excluder._is_relevant_file_to_scan_common(consts.SCA_SCAN_TYPE, file_path) is True + + def test_unrelated_yaml_file_is_not_collected(self) -> None: + """Collecting pnpm-workspace.yaml must not make every yaml file part of an SCA scan.""" + excluder = Excluder() + + assert excluder._is_relevant_file_to_scan_common(consts.SCA_SCAN_TYPE, 'repo/config.yaml') is False