From 4037efb0ddf0a30fe1dbeb2a01801ddbbc6ad881 Mon Sep 17 00:00:00 2001 From: daniel-joica Date: Mon, 9 Nov 2026 15:34:13 +0000 Subject: [PATCH 1/3] feat(cli): push entry-points.json through the EntryPoints endpoint `uipath push` uploaded entry-points.json inside the structural migration, like any other file. Studio Web treats that as a plain file write: it emits a file-modified notification only. The entry-points-changed notification, which designers that invoke the project (for example a Flow node bound to the function) rely on to refresh the project's inputs and outputs, is raised only by `POST api/Project/{id}/FileOperations/EntryPoints`. Push now keeps entry-points.json out of the structural migration and writes it through that endpoint once the source files have landed, so the announced contract always matches the uploaded code. An unchanged file is neither re-uploaded nor re-announced. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014t9fy6ZwVkHzR85DoT8Voq --- packages/uipath/pyproject.toml | 2 +- .../src/uipath/_cli/_push/sw_file_handler.py | 76 ++++- .../src/uipath/_cli/_utils/_studio_project.py | 31 +++ packages/uipath/tests/cli/test_push.py | 260 ++++++++++++++++-- packages/uipath/uv.lock | 8 +- 5 files changed, 353 insertions(+), 24 deletions(-) diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index 7435d721b..92d5a5a4b 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.15.1" +version = "2.15.2" 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/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py b/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py index 3fc264e41..2ab58a5e2 100644 --- a/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py +++ b/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py @@ -11,6 +11,7 @@ from uipath._cli.models.uipath_json_schema import PackOptions from uipath.platform.common import UiPathConfig from uipath.platform.constants import ( + ENTRY_POINTS_FILE, EVALS_FOLDER, LEGACY_EVAL_FOLDER, PYTHON_CONFIGURATION_FILE, @@ -162,15 +163,27 @@ async def _process_file_uploads( ) processed_source_files: set[str] = set() updates: list[UpdateEvent] = [] + # Written through the EntryPoints endpoint once the sources have landed, + # so Studio Web announces a contract that matches the uploaded code. + entry_points_content: str | None = None for local_file in local_files: if not os.path.exists(local_file.file_path): logger.info(f"File not found: '{local_file.file_path}'") continue - remote_file = remote_files.get( - local_file.relative_path.replace("\\", "/"), None - ) + relative_path = local_file.relative_path.replace("\\", "/") + remote_file = remote_files.get(relative_path, None) + + if relative_path == ENTRY_POINTS_FILE: + if remote_file: + processed_source_files.add(remote_file.id) + ( + entry_points_content, + entry_points_event, + ) = await self._prepare_entry_points_file(local_file, remote_file) + updates.append(entry_points_event) + continue if remote_file: processed_source_files.add(remote_file.id) @@ -288,11 +301,68 @@ async def _process_file_uploads( structural_migration ) + if entry_points_content is not None: + await self._studio_client.set_entry_points_async(entry_points_content) + # Clean up empty folders after migration await self._cleanup_empty_folders() return updates + async def _prepare_entry_points_file( + self, + local_file: FileInfo, + remote_file: ProjectFile | None, + ) -> tuple[str | None, UpdateEvent]: + """Decide whether entry-points.json must be written and describe the operation. + + The file is kept out of the structural migration: Studio Web only raises + its entry-points-changed notification for the dedicated EntryPoints + endpoint, and that notification is what lets designers consuming this + project (for example a Flow that invokes it) refresh its inputs and + outputs. A plain file write updates the file silently. + + Args: + local_file: The local entry-points.json. + remote_file: The remote entry-points.json, if the project has one. + + Returns: + The content to send through the EntryPoints endpoint (None when the + remote copy is already identical) and the progress event to report. + """ + with open(local_file.file_path, "r", encoding="utf-8") as f: + local_content = f.read() + + if remote_file is None: + return local_content, UpdateEvent( + file_path=local_file.file_path, + status="uploading", + message=f"Uploading '{local_file.file_name}'", + ) + + try: + remote_response = await self._studio_client.download_project_file_async( + remote_file + ) + remote_content = remote_response.read().decode("utf-8") + if compute_normalized_hash(local_content) == compute_normalized_hash( + remote_content + ): + return None, UpdateEvent( + file_path=local_file.file_path, + status="up_to_date", + message=f"File '{local_file.file_name}' is up to date", + ) + except Exception as e: + logger.warning(f"Failed to compare file '{local_file.file_path}': {e}") + # If comparison fails, proceed with update + + return local_content, UpdateEvent( + file_path=local_file.file_path, + status="updating", + message=f"Updating '{local_file.file_name}'", + ) + def _collect_deleted_files( self, remote_files: dict[str, ProjectFile], diff --git a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py index 84ad03284..f14cc1e58 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py +++ b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py @@ -855,6 +855,37 @@ async def upload_file_async( # response contains only the uploaded file identifier return response.json(), action + @traced(name="set_entry_points", run_type="uipath") + @with_lock_retry + async def set_entry_points_async( + self, + content: str, + headers: Optional[dict[str, Any]] = None, + ) -> str: + """Create or overwrite the project's entry-points.json. + + Studio Web raises its entry-points-changed notification only for writes + made through this endpoint, not for plain file writes. Designers that + consume this project's contract (for example a Flow that invokes it) + refresh their copy of the inputs and outputs from that notification. + + Args: + content: The JSON text of entry-points.json. + headers: HTTP headers (the lock key is injected by the decorator). + + Returns: + str: The id of the entry-points.json file in the project. + """ + request_headers = {**(headers or {}), "Content-Type": "application/json"} + response = await self.uipath.api_client.request_async( + "POST", + url=f"{self.file_operations_base_url}/EntryPoints", + scoped="org", + content=content.encode("utf-8"), + headers=request_headers, + ) + return response.text.strip().strip('"') + @traced(name="delete_file", run_type="uipath") @with_lock_retry async def delete_item_async( diff --git a/packages/uipath/tests/cli/test_push.py b/packages/uipath/tests/cli/test_push.py index 95a870701..2693aee20 100644 --- a/packages/uipath/tests/cli/test_push.py +++ b/packages/uipath/tests/cli/test_push.py @@ -126,6 +126,16 @@ def _mock_file_download( text="Remote file content" if not file_content else file_content, ) + def _mock_entry_points_upload( + self, httpx_mock: HTTPXMock, base_url: str, project_id: str + ): + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) + def _create_required_files(self, exclude: list[str] | None = None): required_files = ["uipath.json", "bindings.json", "entry-points.json"] for file in required_files: @@ -296,7 +306,7 @@ def test_successful_push( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") self._mock_file_download(httpx_mock, "456") @@ -325,6 +335,7 @@ def test_successful_push( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) # Mock empty folder cleanup - get structure again after migration httpx_mock.add_response( @@ -419,7 +430,7 @@ def test_successful_push_new_project( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") @@ -429,6 +440,7 @@ def test_successful_push_new_project( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) # Mock empty folder cleanup - get structure again after migration httpx_mock.add_response( @@ -534,7 +546,7 @@ def test_first_push_with_minimal_studio_metadata_from_init( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -543,6 +555,7 @@ def test_first_push_with_minimal_studio_metadata_from_init( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -810,7 +823,7 @@ def test_push_to_scaffolded_function_project( json={"id": project_id, "projectType": "Function"}, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) httpx_mock.add_response( method="POST", @@ -818,6 +831,7 @@ def test_push_to_scaffolded_function_project( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -947,7 +961,7 @@ def test_push_with_nolock_flag( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") self._mock_file_download(httpx_mock, "789") self._mock_file_download(httpx_mock, "456") @@ -958,6 +972,7 @@ def test_push_with_nolock_flag( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1034,7 +1049,7 @@ def test_push_files_excluded( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1043,6 +1058,7 @@ def test_push_files_excluded( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1113,7 +1129,7 @@ def test_push_files_excluded_takes_precedence_over_included( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1122,6 +1138,7 @@ def test_push_files_excluded_takes_precedence_over_included( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1191,7 +1208,7 @@ def test_push_filename_vs_path_exclusion( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1200,6 +1217,7 @@ def test_push_filename_vs_path_exclusion( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1281,7 +1299,7 @@ def test_push_filename_vs_path_inclusion( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1290,6 +1308,7 @@ def test_push_filename_vs_path_inclusion( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1387,7 +1406,7 @@ def test_push_directory_name_vs_path_exclusion( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) httpx_mock.add_response( method="POST", @@ -1395,6 +1414,7 @@ def test_push_directory_name_vs_path_exclusion( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1507,7 +1527,7 @@ def test_push_detects_source_file_conflicts( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") self._mock_file_download(httpx_mock, "456") @@ -1518,6 +1538,7 @@ def test_push_detects_source_file_conflicts( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) with runner.isolated_filesystem(temp_dir=temp_dir): self._create_required_files() @@ -1594,7 +1615,7 @@ def test_push_shows_up_to_date_for_unchanged_files( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") # Mock file downloads - return same content as local files @@ -1607,6 +1628,7 @@ def test_push_shows_up_to_date_for_unchanged_files( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) with runner.isolated_filesystem(temp_dir=temp_dir): self._create_required_files() @@ -1664,7 +1686,7 @@ def test_push_skips_root_evals_folder_files( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1673,6 +1695,7 @@ def test_push_skips_root_evals_folder_files( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1758,7 +1781,7 @@ def test_push_skips_only_root_evals_files_not_subdirectories( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1767,6 +1790,7 @@ def test_push_skips_only_root_evals_files_not_subdirectories( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1844,7 +1868,7 @@ def test_push_logs_skipped_files_count( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "123") httpx_mock.add_response( @@ -1853,6 +1877,7 @@ def test_push_logs_skipped_files_count( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -1973,7 +1998,7 @@ def test_push_preserves_remote_evals_when_no_local_evals( json=mock_structure, ) - self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) self._mock_file_download(httpx_mock, "main-456") self._mock_file_download(httpx_mock, "123") @@ -1984,6 +2009,7 @@ def test_push_preserves_remote_evals_when_no_local_evals( status_code=200, json={"success": True}, ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) with runner.isolated_filesystem(temp_dir=temp_dir): self._create_required_files() @@ -2022,6 +2048,180 @@ def test_push_preserves_remote_evals_when_no_local_evals( assert b"evaluator-file-1" not in content assert b"eval-set-file-1" not in content + def _mock_structure_with_entry_points( + self, httpx_mock: HTTPXMock, base_url: str, project_id: str + ) -> None: + mock_structure = { + "id": "root", + "name": "root", + "folders": [], + "files": [ + { + "id": "123", + "name": "main.py", + "isMain": True, + "fileType": "1", + "isEntryPoint": True, + "ignoredFromPublish": False, + }, + { + "id": "456", + "name": "pyproject.toml", + "isMain": False, + "fileType": "1", + "isEntryPoint": False, + "ignoredFromPublish": False, + }, + { + "id": "898", + "name": "entry-points.json", + "isMain": False, + "fileType": "1", + "isEntryPoint": False, + "ignoredFromPublish": False, + }, + ], + "folderType": "0", + } + # Fetched before the upload and again by the empty-folder cleanup. + for _ in range(2): + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json=mock_structure, + ) + + def test_push_writes_entry_points_through_entry_points_endpoint( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + mock_env_vars: dict[str, str], + httpx_mock: HTTPXMock, + ) -> None: + """entry-points.json goes through the EntryPoints endpoint, after the structural migration.""" + base_url = "https://cloud.uipath.com/organization" + project_id = "test-project-id" + local_entry_points = { + "$schema": "https://cloud.uipath.com/draft/2024-12/entry-point", + "entryPoints": [ + { + "filePath": "main", + "uniqueId": "c2a0b6a2-5e0e-4a4a-9a7a-2c5a3f6a1b10", + "type": "agent", + "input": { + "type": "object", + "properties": {"topic": {"type": "string"}}, + }, + "output": {"type": "object", "properties": {}}, + } + ], + } + + self._mock_structure_with_entry_points(httpx_mock, base_url, project_id) + # One lock lookup for the structural migration, one for the entry-points write. + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=2) + self._mock_file_download(httpx_mock, "123") + self._mock_file_download(httpx_mock, "456") + self._mock_file_download(httpx_mock, "898", file_content='{"entryPoints": []}') + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration", + status_code=200, + json={"success": True}, + ) + self._mock_entry_points_upload(httpx_mock, base_url, project_id) + + with runner.isolated_filesystem(temp_dir=temp_dir): + self._create_required_files(exclude=["entry-points.json"]) + with open("entry-points.json", "w") as f: + json.dump(local_entry_points, f) + 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')") + + 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, result.output + assert "Updating 'entry-points.json'" in result.output + + entry_points_url = f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints" + migration_url = f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration" + + entry_points_request = httpx_mock.get_request( + method="POST", url=entry_points_url + ) + assert entry_points_request is not None + assert json.loads(entry_points_request.content) == local_entry_points + assert entry_points_request.headers["Content-Type"] == "application/json" + assert ( + entry_points_request.headers["x-uipath-sw-lockkey"] == "test-lock-key" + ) + + migration_request = httpx_mock.get_request(method="POST", url=migration_url) + assert migration_request is not None + assert b"entry-points.json" not in migration_request.content + assert ( + b"ModifiedResources[0].Id\r\n\r\n898" not in migration_request.content + ) + + ordered_urls = [str(request.url) for request in httpx_mock.get_requests()] + assert ordered_urls.index(migration_url) < ordered_urls.index( + entry_points_url + ) + + def test_push_skips_entry_points_endpoint_when_unchanged( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + mock_env_vars: dict[str, str], + httpx_mock: HTTPXMock, + ) -> None: + """An unchanged entry-points.json is neither migrated nor re-announced.""" + base_url = "https://cloud.uipath.com/organization" + project_id = "test-project-id" + local_entry_points = {"entryPoints": [{"filePath": "main", "type": "agent"}]} + + self._mock_structure_with_entry_points(httpx_mock, base_url, project_id) + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + self._mock_file_download(httpx_mock, "123") + self._mock_file_download(httpx_mock, "456") + self._mock_file_download( + httpx_mock, "898", file_content=json.dumps(local_entry_points, indent=4) + ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration", + status_code=200, + json={"success": True}, + ) + + with runner.isolated_filesystem(temp_dir=temp_dir): + self._create_required_files(exclude=["entry-points.json"]) + with open("entry-points.json", "w") as f: + json.dump(local_entry_points, f) + 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')") + + 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, result.output + assert "File 'entry-points.json' is up to date" in result.output + assert ( + httpx_mock.get_request( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + ) + is None + ) + class TestResourceCreation: """Test resource creation and import functionality during push.""" @@ -2099,6 +2299,7 @@ def test_push_with_resources_imports_referenced_resources( "projectLockKey": "test-lock-key", "solutionLockKey": "test-solution-lock-key", }, + is_reusable=True, ) httpx_mock.add_response( @@ -2107,6 +2308,12 @@ def test_push_with_resources_imports_referenced_resources( status_code=200, json={"success": True}, ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -2293,6 +2500,7 @@ def test_push_with_ignore_resources_flag_skips_resource_import( "projectLockKey": "test-lock-key", "solutionLockKey": "test-solution-lock-key", }, + is_reusable=True, ) httpx_mock.add_response( @@ -2301,6 +2509,12 @@ def test_push_with_ignore_resources_flag_skips_resource_import( status_code=200, json={"success": True}, ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -2407,6 +2621,7 @@ def test_push_with_resource_not_found_creates_virtual( "projectLockKey": "test-lock-key", "solutionLockKey": "test-solution-lock-key", }, + is_reusable=True, ) httpx_mock.add_response( @@ -2415,6 +2630,12 @@ def test_push_with_resource_not_found_creates_virtual( status_code=200, json={"success": True}, ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", @@ -2576,6 +2797,7 @@ def test_push_with_resource_already_exists_shows_unchanged( "projectLockKey": "test-lock-key", "solutionLockKey": "test-solution-lock-key", }, + is_reusable=True, ) self._mock_file_download(httpx_mock, "123") @@ -2585,6 +2807,12 @@ def test_push_with_resource_already_exists_shows_unchanged( status_code=200, json={"success": True}, ) + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/EntryPoints", + status_code=200, + text='"entry-points-file-id"', + ) httpx_mock.add_response( url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 7cdb06e88..0740725cd 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 5 +revision = 3 requires-python = ">=3.11" [options] @@ -7,10 +7,10 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P2D" [options.exclude-newer-package] -uipath-core = false uipath-ipc = false -uipath-platform = false uipath-runtime = false +uipath-platform = false +uipath-core = false [[package]] name = "aiohappyeyeballs" @@ -2599,7 +2599,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.15.1" +version = "2.15.2" source = { editable = "." } dependencies = [ { name = "applicationinsights" }, From 7c0d50c1e36acc9d9f810539c738b2843345b0fa Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 15:35:10 +0000 Subject: [PATCH 2/3] chore(cli): keep uv.lock changes to the version bump The previous commit's `uv lock` run came from a different uv release and rewrote the lock revision and key order. Only the package version changes. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014t9fy6ZwVkHzR85DoT8Voq --- packages/uipath/uv.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index 0740725cd..4d69c7a08 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 5 requires-python = ">=3.11" [options] @@ -7,10 +7,10 @@ exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for exclude-newer-span = "P2D" [options.exclude-newer-package] +uipath-core = false uipath-ipc = false -uipath-runtime = false uipath-platform = false -uipath-core = false +uipath-runtime = false [[package]] name = "aiohappyeyeballs" From 85918ad33baa4bec017d60a9316301c6b01fd613 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 9 Nov 2026 17:38:37 +0000 Subject: [PATCH 3/3] fix(cli): read entry-points.json asynchronously in the push handler Uses the anyio.Path read the CLI already relies on elsewhere, which also clears the SonarCloud finding on blocking open() in an async function. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_014t9fy6ZwVkHzR85DoT8Voq --- packages/uipath/src/uipath/_cli/_push/sw_file_handler.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py b/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py index 2ab58a5e2..92fc267f9 100644 --- a/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py +++ b/packages/uipath/src/uipath/_cli/_push/sw_file_handler.py @@ -6,6 +6,7 @@ from datetime import datetime, timezone from typing import AsyncIterator +import anyio import click from uipath._cli.models.uipath_json_schema import PackOptions @@ -330,8 +331,9 @@ async def _prepare_entry_points_file( The content to send through the EntryPoints endpoint (None when the remote copy is already identical) and the progress event to report. """ - with open(local_file.file_path, "r", encoding="utf-8") as f: - local_content = f.read() + local_content = await anyio.Path(local_file.file_path).read_text( + encoding="utf-8" + ) if remote_file is None: return local_content, UpdateEvent(