diff --git a/packages/uipath/pyproject.toml b/packages/uipath/pyproject.toml index ea18bed82..0df94ca0c 100644 --- a/packages/uipath/pyproject.toml +++ b/packages/uipath/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "uipath" -version = "2.13.11" +version = "2.13.12" 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/_utils/_studio_project.py b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py index 3a60fc3ca..10eccdf5d 100644 --- a/packages/uipath/src/uipath/_cli/_utils/_studio_project.py +++ b/packages/uipath/src/uipath/_cli/_utils/_studio_project.py @@ -513,6 +513,9 @@ async def _get_solution_id(self) -> str: async def ensure_coded_agent_project_async(self): structure = await self.get_project_structure_async() + # An empty structure means a never-pushed project: allow the first push. + if not structure.files and not structure.folders: + return if not any(file.name == PYTHON_CONFIGURATION_FILE for file in structure.files): raise NonCodedAgentProjectException() @@ -710,13 +713,22 @@ async def get_project_structure_async( if not force and self._project_structure_cache is not None: return self._project_structure_cache - response = await self.uipath.api_client.request_async( - "GET", - url=f"{self.file_operations_base_url}/Structure", - scoped="org", - ) - - self._project_structure_cache = ProjectStructure.model_validate(response.json()) + try: + response = await self.uipath.api_client.request_async( + "GET", + url=f"{self.file_operations_base_url}/Structure", + scoped="org", + ) + structure = ProjectStructure.model_validate(response.json()) + except EnrichedException as e: + # The backend returns 404 for projects whose file system was never + # initialized (e.g. a freshly created Function project): treat it + # as an empty structure so the first push can bootstrap the files. + if e.status_code != 404: + raise + structure = ProjectStructure(name="root", folders=[], files=[]) + + self._project_structure_cache = structure return self._project_structure_cache @traced(name="create_folder", run_type="uipath") diff --git a/packages/uipath/tests/cli/test_push.py b/packages/uipath/tests/cli/test_push.py index d4d30a166..a9af55f14 100644 --- a/packages/uipath/tests/cli/test_push.py +++ b/packages/uipath/tests/cli/test_push.py @@ -712,6 +712,80 @@ def test_push_non_coded_agent_project( in result.output ) + def test_first_push_to_uninitialized_project( + self, + runner: CliRunner, + temp_dir: str, + project_details: ProjectDetails, + mock_env_vars: dict[str, str], + httpx_mock: HTTPXMock, + ) -> None: + """Test push when the remote file system was never initialized. + + The backend returns 404 from FileOperations/Structure for projects + whose file system does not exist yet (e.g. a freshly created Function + project). The first push should bootstrap the files instead of failing + the coded-agent validation. + """ + base_url = "https://cloud.uipath.com/organization" + project_id = "test-project-id" + + # Uninitialized file system: Structure returns 404 + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + status_code=404, + json={ + "type": "https://tools.ietf.org/html/rfc9110#section-15.5.5", + "title": "Not Found", + "status": 404, + }, + ) + + self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1) + + httpx_mock.add_response( + method="POST", + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration", + status_code=200, + json={"success": True}, + ) + + # Empty folder cleanup - get structure again after migration + httpx_mock.add_response( + url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure", + json={ + "id": "root", + "name": "root", + "folders": [], + "files": [], + "folderType": "0", + }, + ) + + 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')") + + with open("uv.lock", "w") as f: + f.write('version = 1 \n requires-python = ">=3.11"') + + 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 + assert "not of type coded agent" not in result.output + assert "Uploading 'main.py'" in result.output + assert "Uploading 'pyproject.toml'" in result.output + assert "Uploading 'uipath.json'" in result.output + assert "Uploading 'uv.lock'" in result.output + assert "Uploading '.uipath/studio_metadata.json'" in result.output + def test_push_with_nolock_flag( self, runner: CliRunner, diff --git a/packages/uipath/uv.lock b/packages/uipath/uv.lock index aea72e7a1..f103aeec0 100644 --- a/packages/uipath/uv.lock +++ b/packages/uipath/uv.lock @@ -2598,7 +2598,7 @@ wheels = [ [[package]] name = "uipath" -version = "2.13.11" +version = "2.13.12" source = { editable = "." } dependencies = [ { name = "applicationinsights" },