Skip to content

Commit 487bf03

Browse files
authored
fix: allow first push to a project with uninitialized file system (#1820)
1 parent e2ab6f7 commit 487bf03

4 files changed

Lines changed: 95 additions & 9 deletions

File tree

packages/uipath/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath"
3-
version = "2.13.11"
3+
version = "2.13.12"
44
description = "Python SDK and CLI for UiPath Platform, enabling programmatic interaction with automation services, process management, and deployment tools."
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

packages/uipath/src/uipath/_cli/_utils/_studio_project.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -513,6 +513,9 @@ async def _get_solution_id(self) -> str:
513513

514514
async def ensure_coded_agent_project_async(self):
515515
structure = await self.get_project_structure_async()
516+
# An empty structure means a never-pushed project: allow the first push.
517+
if not structure.files and not structure.folders:
518+
return
516519
if not any(file.name == PYTHON_CONFIGURATION_FILE for file in structure.files):
517520
raise NonCodedAgentProjectException()
518521

@@ -710,13 +713,22 @@ async def get_project_structure_async(
710713
if not force and self._project_structure_cache is not None:
711714
return self._project_structure_cache
712715

713-
response = await self.uipath.api_client.request_async(
714-
"GET",
715-
url=f"{self.file_operations_base_url}/Structure",
716-
scoped="org",
717-
)
718-
719-
self._project_structure_cache = ProjectStructure.model_validate(response.json())
716+
try:
717+
response = await self.uipath.api_client.request_async(
718+
"GET",
719+
url=f"{self.file_operations_base_url}/Structure",
720+
scoped="org",
721+
)
722+
structure = ProjectStructure.model_validate(response.json())
723+
except EnrichedException as e:
724+
# The backend returns 404 for projects whose file system was never
725+
# initialized (e.g. a freshly created Function project): treat it
726+
# as an empty structure so the first push can bootstrap the files.
727+
if e.status_code != 404:
728+
raise
729+
structure = ProjectStructure(name="root", folders=[], files=[])
730+
731+
self._project_structure_cache = structure
720732
return self._project_structure_cache
721733

722734
@traced(name="create_folder", run_type="uipath")

packages/uipath/tests/cli/test_push.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -712,6 +712,80 @@ def test_push_non_coded_agent_project(
712712
in result.output
713713
)
714714

715+
def test_first_push_to_uninitialized_project(
716+
self,
717+
runner: CliRunner,
718+
temp_dir: str,
719+
project_details: ProjectDetails,
720+
mock_env_vars: dict[str, str],
721+
httpx_mock: HTTPXMock,
722+
) -> None:
723+
"""Test push when the remote file system was never initialized.
724+
725+
The backend returns 404 from FileOperations/Structure for projects
726+
whose file system does not exist yet (e.g. a freshly created Function
727+
project). The first push should bootstrap the files instead of failing
728+
the coded-agent validation.
729+
"""
730+
base_url = "https://cloud.uipath.com/organization"
731+
project_id = "test-project-id"
732+
733+
# Uninitialized file system: Structure returns 404
734+
httpx_mock.add_response(
735+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure",
736+
status_code=404,
737+
json={
738+
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
739+
"title": "Not Found",
740+
"status": 404,
741+
},
742+
)
743+
744+
self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1)
745+
746+
httpx_mock.add_response(
747+
method="POST",
748+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration",
749+
status_code=200,
750+
json={"success": True},
751+
)
752+
753+
# Empty folder cleanup - get structure again after migration
754+
httpx_mock.add_response(
755+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure",
756+
json={
757+
"id": "root",
758+
"name": "root",
759+
"folders": [],
760+
"files": [],
761+
"folderType": "0",
762+
},
763+
)
764+
765+
with runner.isolated_filesystem(temp_dir=temp_dir):
766+
self._create_required_files()
767+
768+
with open("pyproject.toml", "w") as f:
769+
f.write(project_details.to_toml())
770+
771+
with open("main.py", "w") as f:
772+
f.write("print('Hello World')")
773+
774+
with open("uv.lock", "w") as f:
775+
f.write('version = 1 \n requires-python = ">=3.11"')
776+
777+
configure_env_vars(mock_env_vars)
778+
os.environ["UIPATH_PROJECT_ID"] = project_id
779+
780+
result = runner.invoke(cli, ["push", "./", "--ignore-resources"])
781+
assert result.exit_code == 0
782+
assert "not of type coded agent" not in result.output
783+
assert "Uploading 'main.py'" in result.output
784+
assert "Uploading 'pyproject.toml'" in result.output
785+
assert "Uploading 'uipath.json'" in result.output
786+
assert "Uploading 'uv.lock'" in result.output
787+
assert "Uploading '.uipath/studio_metadata.json'" in result.output
788+
715789
def test_push_with_nolock_flag(
716790
self,
717791
runner: CliRunner,

packages/uipath/uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)