Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath/pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
26 changes: 19 additions & 7 deletions packages/uipath/src/uipath/_cli/_utils/_studio_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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=[])

Comment on lines +716 to +730
self._project_structure_cache = structure
return self._project_structure_cache

@traced(name="create_folder", run_type="uipath")
Expand Down
74 changes: 74 additions & 0 deletions packages/uipath/tests/cli/test_push.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/uipath/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading