From 9f327552c1e9233bdeddcabbb8a852b42738e02a Mon Sep 17 00:00:00 2001 From: Anipik Date: Fri, 20 Feb 2026 15:15:08 -0800 Subject: [PATCH 1/3] fix: log telemetry failure and skip files in eval --- pyproject.toml | 2 +- src/uipath/_cli/_push/sw_file_handler.py | 14 +++++++++- src/uipath/_cli/_utils/_project_files.py | 18 ++++++++++--- src/uipath/_cli/cli_pack.py | 2 +- src/uipath/telemetry/_track.py | 33 ++++++++++++++---------- uv.lock | 2 +- 6 files changed, 51 insertions(+), 20 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3926352b8..bbf98012a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.8.48" +version = "2.8.49" description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools." readme = { file = "README.md", content-type = "text/markdown" } requires-python = ">=3.11" diff --git a/src/uipath/_cli/_push/sw_file_handler.py b/src/uipath/_cli/_push/sw_file_handler.py index 891c8b4e1..3a7f23d25 100644 --- a/src/uipath/_cli/_push/sw_file_handler.py +++ b/src/uipath/_cli/_push/sw_file_handler.py @@ -527,12 +527,24 @@ async def upload_source_files( remote_files = self._get_remote_files(structure) # Get files to upload and process them - local_files = files_to_include( + local_files, skipped_files = files_to_include( pack_options, self.directory, self.include_uv_lock, ) + # Log skipped files from root evals folder + if skipped_files: + logger.info( + f"Skipping {len(skipped_files)} file(s) in root evals folder: {', '.join(skipped_files)}" + ) + for skipped_file in skipped_files: + yield UpdateEvent( + file_path=skipped_file, + status="skipped", + message=f"Skipping '{skipped_file}' (root evals folder)", + ) + updates = await self._process_file_uploads(local_files, remote_files) # Yield all updates diff --git a/src/uipath/_cli/_utils/_project_files.py b/src/uipath/_cli/_utils/_project_files.py index 85664bef9..01ec36de5 100644 --- a/src/uipath/_cli/_utils/_project_files.py +++ b/src/uipath/_cli/_utils/_project_files.py @@ -373,7 +373,7 @@ def files_to_include( directory: str, include_uv_lock: bool = True, directories_to_ignore: list[str] | None = None, -) -> list[FileInfo]: +) -> tuple[list[FileInfo], list[str]]: """Get list of files to include in the project based on configuration. Walks through the directory tree and identifies files to include based on extensions @@ -386,7 +386,7 @@ def files_to_include( directories_to_ignore: List of directories to ignore Returns: - list[FileInfo]: List of file information objects for included files + tuple[list[FileInfo], list[str]]: Tuple of (included files, skipped file paths) """ file_extensions_included = [".py", ".mermaid", ".json", ".yaml", ".yml", ".md"] files_included = ["pyproject.toml"] @@ -422,8 +422,15 @@ def is_venv_dir(d: str) -> bool: ) extra_files: list[FileInfo] = [] + skipped_files: list[str] = [] + # Walk through directory and return all files in the allowlist for root, dirs, files in os.walk(directory): + # Determine if we're in the root evals folder + root_rel_path = os.path.relpath(root, directory) + normalized_root_rel_path = root_rel_path.replace(os.sep, "/") + is_root_evals_folder = normalized_root_rel_path == "evals" + # Skip all directories that start with . or are a venv or are excluded included_dirs = [] for d in dirs: @@ -461,6 +468,11 @@ def is_venv_dir(d: str) -> bool: # Normalize the path normalized_rel_path = rel_path.replace(os.sep, "/") + # Skip files in the root evals folder (but allow eval-set and evaluators subdirectories) + if is_root_evals_folder: + skipped_files.append(normalized_rel_path) + continue + # Check inclusion: by extension, by filename (for base directory), or by relative path should_include = ( file_extension in file_extensions_included @@ -489,7 +501,7 @@ def is_venv_dir(d: str) -> bool: is_binary=is_binary_file(file_extension), ) ) - return extra_files + return extra_files, skipped_files def compute_normalized_hash(content: str) -> str: diff --git a/src/uipath/_cli/cli_pack.py b/src/uipath/_cli/cli_pack.py index fddef5c0c..f91145f53 100644 --- a/src/uipath/_cli/cli_pack.py +++ b/src/uipath/_cli/cli_pack.py @@ -283,7 +283,7 @@ def pack_fn( z.writestr(f"{project_name}.nuspec", nuspec_content) z.writestr("_rels/.rels", rels_content) - files = files_to_include( + files, _ = files_to_include( config_data.pack_options, directory, include_uv_lock, diff --git a/src/uipath/telemetry/_track.py b/src/uipath/telemetry/_track.py index 926fb7dc5..893ee6456 100644 --- a/src/uipath/telemetry/_track.py +++ b/src/uipath/telemetry/_track.py @@ -161,9 +161,10 @@ def _initialize() -> None: # Set application version _AppInsightsEventClient._client.context.application.ver = version("uipath") - except Exception: - # Silently fail - telemetry should never break the main application - pass + except Exception as e: + # Log but don't raise - telemetry should never break the main application + _logger.warning(f"Failed to initialize Application Insights client: {e}") + _logger.debug("Application Insights initialization error", exc_info=True) @staticmethod def track_event( @@ -193,9 +194,10 @@ def track_event( ) # Note: We don't flush after every event to avoid blocking. # Events will be sent in batches by the SDK. - except Exception: - # Telemetry should never break the main application - pass + except Exception as e: + # Log but don't raise - telemetry should never break the main application + _logger.warning(f"Failed to track event '{name}': {e}") + _logger.debug(f"Event tracking error for '{name}'", exc_info=True) @staticmethod def flush() -> None: @@ -203,8 +205,10 @@ def flush() -> None: if _AppInsightsEventClient._client: try: _AppInsightsEventClient._client.flush() - except Exception: - pass + except Exception as e: + # Log but don't raise - telemetry should never break the main application + _logger.warning(f"Failed to flush telemetry events: {e}") + _logger.debug("Telemetry flush error", exc_info=True) class _TelemetryClient: @@ -238,8 +242,10 @@ def _initialize(): _logger.setLevel(INFO) _TelemetryClient._initialized = True - except Exception: - pass + except Exception as e: + # Log but don't raise - telemetry should never break the main application + getLogger(__name__).warning(f"Failed to initialize telemetry client: {e}") + getLogger(__name__).debug("Telemetry initialization error", exc_info=True) @staticmethod def _track_method(name: str, attrs: Optional[Dict[str, Any]] = None): @@ -278,9 +284,10 @@ def track_event( try: _AppInsightsEventClient.track_event(name, properties) - except Exception: - # Telemetry should never break the main application - pass + except Exception as e: + # Log but don't raise - telemetry should never break the main application + _logger.warning(f"Failed to track event '{name}': {e}") + _logger.debug(f"Event tracking error for '{name}'", exc_info=True) def track_event( diff --git a/uv.lock b/uv.lock index 8550c39fc..e66b1cee3 100644 --- a/uv.lock +++ b/uv.lock @@ -2531,7 +2531,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.8.48" +version = "2.8.49" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 7aa5c28ab2f4b42aef75e498f2cd888dabd6cf5a Mon Sep 17 00:00:00 2001 From: Anipik Date: Fri, 20 Feb 2026 15:42:39 -0800 Subject: [PATCH 2/3] fix: address PR review comments - Use _logger instead of getLogger(__name__) in telemetry initialization - Make unpacking named in cli_pack.py (skipped_files instead of _) - Simplify message and show actual folder path in sw_file_handler.py --- src/uipath/_cli/_push/sw_file_handler.py | 5 +++-- src/uipath/_cli/cli_pack.py | 2 +- src/uipath/telemetry/_track.py | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/uipath/_cli/_push/sw_file_handler.py b/src/uipath/_cli/_push/sw_file_handler.py index 3a7f23d25..504d1019d 100644 --- a/src/uipath/_cli/_push/sw_file_handler.py +++ b/src/uipath/_cli/_push/sw_file_handler.py @@ -535,14 +535,15 @@ async def upload_source_files( # Log skipped files from root evals folder if skipped_files: + evals_folder_path = os.path.join(self.directory, "evals") logger.info( - f"Skipping {len(skipped_files)} file(s) in root evals folder: {', '.join(skipped_files)}" + f"Skipping {len(skipped_files)} file(s) in evals folder ({evals_folder_path}): {', '.join(skipped_files)}" ) for skipped_file in skipped_files: yield UpdateEvent( file_path=skipped_file, status="skipped", - message=f"Skipping '{skipped_file}' (root evals folder)", + message=f"Skipping '{skipped_file}' (evals folder)", ) updates = await self._process_file_uploads(local_files, remote_files) diff --git a/src/uipath/_cli/cli_pack.py b/src/uipath/_cli/cli_pack.py index f91145f53..69e5c5a94 100644 --- a/src/uipath/_cli/cli_pack.py +++ b/src/uipath/_cli/cli_pack.py @@ -283,7 +283,7 @@ def pack_fn( z.writestr(f"{project_name}.nuspec", nuspec_content) z.writestr("_rels/.rels", rels_content) - files, _ = files_to_include( + files, skipped_files = files_to_include( config_data.pack_options, directory, include_uv_lock, diff --git a/src/uipath/telemetry/_track.py b/src/uipath/telemetry/_track.py index 893ee6456..f2ecf2cca 100644 --- a/src/uipath/telemetry/_track.py +++ b/src/uipath/telemetry/_track.py @@ -244,8 +244,8 @@ def _initialize(): _TelemetryClient._initialized = True except Exception as e: # Log but don't raise - telemetry should never break the main application - getLogger(__name__).warning(f"Failed to initialize telemetry client: {e}") - getLogger(__name__).debug("Telemetry initialization error", exc_info=True) + _logger.warning(f"Failed to initialize telemetry client: {e}") + _logger.debug("Telemetry initialization error", exc_info=True) @staticmethod def _track_method(name: str, attrs: Optional[Dict[str, Any]] = None): From 9705a0569b58661c502b96a4355582f176711130 Mon Sep 17 00:00:00 2001 From: Anipik Date: Fri, 20 Feb 2026 15:50:30 -0800 Subject: [PATCH 3/3] fix: add tests --- tests/cli/test_push.py | 257 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) diff --git a/tests/cli/test_push.py b/tests/cli/test_push.py index 507800560..67274920d 100644 --- a/tests/cli/test_push.py +++ b/tests/cli/test_push.py @@ -1328,6 +1328,263 @@ def test_push_shows_up_to_date_for_unchanged_files( assert "Updating 'main.py'" not in result.output assert "Updating 'helper.py'" not in result.output + def test_push_skips_root_evals_folder_files( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + mock_env_vars: dict[str, str], + httpx_mock: HTTPXMock, + ) -> None: + """Test that files in root evals folder are skipped during push.""" + base_url = "https://cloud.uipath.com/organization" + project_id = "test-project-id" + + mock_structure = { + "id": "root", + "name": "root", + "folders": [], + "files": [ + { + "id": "123", + "name": "pyproject.toml", + "isMain": False, + "fileType": "1", + "isEntryPoint": False, + "ignoredFromPublish": False, + }, + ], + "folderType": "0", + } + + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json=mock_structure, + ) + + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_file_download(httpx_mock, "123") + + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration", + status_code=200, + json={"success": True}, + ) + + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json=mock_structure, + ) + + with runner.isolated_filesystem(temp_dir=temp_dir): + self._create_required_files() + + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + + with open("main.py", "w") as f: + f.write("print('Hello World')") + + # Create evals folder structure + os.makedirs("evals") + os.makedirs("evals/eval-sets") + os.makedirs("evals/evaluators") + + # Create files in root evals folder (should be skipped) + with open("evals/root_file1.json", "w") as f: + f.write('{"should": "be skipped"}') + with open("evals/root_file2.py", "w") as f: + f.write("print('should be skipped')") + + # Create files in eval-sets (should be included) + with open("evals/eval-sets/test_eval.json", "w") as f: + f.write('{"should": "be included"}') + + # Create files in evaluators (should be included) + with open("evals/evaluators/test_evaluator.py", "w") as f: + f.write("def evaluate(): pass") + + configure_env_vars(mock_env_vars) + os.environ["UIPATH_PROJECT_ID"] = project_id + + result = runner.invoke(cli, ["push", "./", "--ignore-resources"]) + assert result.exit_code == 0 + + # Verify root evals files are skipped + assert "Skipping 'evals/root_file1.json' (evals folder)" in result.output + assert "Skipping 'evals/root_file2.py' (evals folder)" in result.output + + # Verify eval-sets and evaluators files are uploaded + assert "Uploading 'test_eval.json'" in result.output + assert "Uploading 'test_evaluator.py'" in result.output + + # Verify main.py is uploaded + assert "Uploading 'main.py'" in result.output + + def test_push_skips_only_root_evals_files_not_subdirectories( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + mock_env_vars: dict[str, str], + httpx_mock: HTTPXMock, + ) -> None: + """Test that only files directly in evals folder are skipped, not files in subdirectories.""" + base_url = "https://cloud.uipath.com/organization" + project_id = "test-project-id" + + mock_structure = { + "id": "root", + "name": "root", + "folders": [], + "files": [ + { + "id": "123", + "name": "pyproject.toml", + "isMain": False, + "fileType": "1", + "isEntryPoint": False, + "ignoredFromPublish": False, + }, + ], + "folderType": "0", + } + + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json=mock_structure, + ) + + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_file_download(httpx_mock, "123") + + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration", + status_code=200, + json={"success": True}, + ) + + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json=mock_structure, + ) + + with runner.isolated_filesystem(temp_dir=temp_dir): + self._create_required_files() + + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + + # Create evals folder with various subdirectories + os.makedirs("evals/eval-sets") + os.makedirs("evals/evaluators") + os.makedirs("evals/custom-subfolder") + + # Files directly in evals (should be skipped) + with open("evals/config.json", "w") as f: + f.write('{"root": "file"}') + + # Files in subdirectories (should be included) + with open("evals/eval-sets/test.json", "w") as f: + f.write('{"eval": "set"}') + with open("evals/evaluators/eval.py", "w") as f: + f.write("def evaluate(): pass") + with open("evals/custom-subfolder/data.json", "w") as f: + f.write('{"custom": "data"}') + + configure_env_vars(mock_env_vars) + os.environ["UIPATH_PROJECT_ID"] = project_id + + result = runner.invoke(cli, ["push", "./", "--ignore-resources"]) + assert result.exit_code == 0 + + # Verify root file is skipped + assert "Skipping 'evals/config.json' (evals folder)" in result.output + + # Verify subdirectory files are included + assert "Uploading 'test.json'" in result.output + assert "Uploading 'eval.py'" in result.output + assert "Uploading 'data.json'" in result.output + + def test_push_logs_skipped_files_count( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + mock_env_vars: dict[str, str], + httpx_mock: HTTPXMock, + ) -> None: + """Test that push logs the count of skipped files from root evals folder.""" + base_url = "https://cloud.uipath.com/organization" + project_id = "test-project-id" + + mock_structure = { + "id": "root", + "name": "root", + "folders": [], + "files": [ + { + "id": "123", + "name": "pyproject.toml", + "isMain": False, + "fileType": "1", + "isEntryPoint": False, + "ignoredFromPublish": False, + }, + ], + "folderType": "0", + } + + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json=mock_structure, + ) + + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_file_download(httpx_mock, "123") + + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration", + status_code=200, + json={"success": True}, + ) + + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json=mock_structure, + ) + + with runner.isolated_filesystem(temp_dir=temp_dir): + self._create_required_files() + + with open("pyproject.toml", "w") as f: + f.write(project_details.to_toml()) + + # Create evals folder with multiple files + os.makedirs("evals") + + # Create 3 files in root evals + with open("evals/file1.json", "w") as f: + f.write("{}") + with open("evals/file2.py", "w") as f: + f.write("pass") + with open("evals/file3.yaml", "w") as f: + f.write("key: value") + + configure_env_vars(mock_env_vars) + os.environ["UIPATH_PROJECT_ID"] = project_id + + result = runner.invoke(cli, ["push", "./", "--ignore-resources"]) + assert result.exit_code == 0 + + # Verify individual files are skipped + assert "Skipping 'evals/file1.json' (evals folder)" in result.output + assert "Skipping 'evals/file2.py' (evals folder)" in result.output + assert "Skipping 'evals/file3.yaml' (evals folder)" in result.output + def test_push_preserves_remote_evals_when_no_local_evals( self, runner: CliRunner,