Skip to content

Commit 8fbbb58

Browse files
vldcmp-uipathclaude
andcommitted
fix(cli): raise on 404 from Studio Web project Structure
get_project_structure_async turned any 404 from FileOperations/Structure into an empty ProjectStructure, so a failed fetch was indistinguishable from a fresh project and the push gate allowed the push. Studio Web now creates project.uiproj with every new project, so a 404 only means the project id or the base URL is wrong (e.g. UIPATH_URL without the organization segment). Propagate the error, carrying the request URL and status, and drop the matching 404 fallback in the push file handler. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 4d9a83e commit 8fbbb58

5 files changed

Lines changed: 33 additions & 70 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.14.36"
3+
version = "2.14.37"
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/_push/sw_file_handler.py‎

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
STUDIO_METADATA_FILE,
1818
)
1919

20-
from ...platform.errors import EnrichedException
2120
from .._utils._common import get_claim_from_token
2221
from .._utils._console import ConsoleLogger
2322
from .._utils._constants import (
@@ -512,7 +511,7 @@ async def upload_source_files(
512511
"""Main method to upload source files to the UiPath project.
513512
514513
This method:
515-
1. Gets project structure (or creates if it doesn't exist)
514+
1. Gets the remote project structure
516515
2. Collects local files to upload
517516
3. Processes file uploads (yields progress updates)
518517
4. Performs structural migration
@@ -525,18 +524,12 @@ async def upload_source_files(
525524
FileOperationUpdate: Progress updates for each file operation
526525
527526
Raises:
528-
Exception: If any step in the process fails
527+
EnrichedException: If the project structure cannot be fetched
528+
(including 404, which means the project id or base URL is wrong,
529+
not that the project is empty)
530+
Exception: If any other step in the process fails
529531
"""
530-
# Get or create project structure
531-
try:
532-
structure = await self._studio_client.get_project_structure_async()
533-
except EnrichedException as e:
534-
if e.status_code == 404:
535-
# Project structure doesn't exist - create empty structure and lock
536-
structure = ProjectStructure(name="", files=[], folders=[])
537-
await self._studio_client._put_lock()
538-
else:
539-
raise
532+
structure = await self._studio_client.get_project_structure_async()
540533

541534
remote_files = self._get_remote_files(structure)
542535

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

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -749,26 +749,21 @@ async def get_project_structure_async(
749749
750750
Returns:
751751
ProjectStructure: The complete project structure
752+
753+
Raises:
754+
EnrichedException: If the request fails with a non-2xx status,
755+
including 404 when the project (or the base URL) is wrong.
752756
"""
753757
if not force and self._project_structure_cache is not None:
754758
return self._project_structure_cache
755759

756-
try:
757-
response = await self.uipath.api_client.request_async(
758-
"GET",
759-
url=f"{self.file_operations_base_url}/Structure",
760-
scoped="org",
761-
)
762-
structure = ProjectStructure.model_validate(response.json())
763-
except EnrichedException as e:
764-
# The backend returns 404 for projects whose file system was never
765-
# initialized (e.g. a freshly created Function project): treat it
766-
# as an empty structure so the first push can bootstrap the files.
767-
if e.status_code != 404:
768-
raise
769-
structure = ProjectStructure(name="root", folders=[], files=[])
770-
771-
self._project_structure_cache = structure
760+
# A 404 propagates: Studio Web now creates project.uiproj with every project.
761+
response = await self.uipath.api_client.request_async(
762+
"GET",
763+
url=f"{self.file_operations_base_url}/Structure",
764+
scoped="org",
765+
)
766+
self._project_structure_cache = ProjectStructure.model_validate(response.json())
772767
return self._project_structure_cache
773768

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

‎packages/uipath/tests/cli/test_push.py‎

Lines changed: 14 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -717,27 +717,28 @@ def test_push_non_coded_agent_project(
717717
in result.output
718718
)
719719

720-
def test_first_push_to_uninitialized_project(
720+
def test_push_fails_when_structure_returns_404(
721721
self,
722722
runner: CliRunner,
723723
temp_dir: str,
724724
project_details: ProjectDetails,
725725
mock_env_vars: dict[str, str],
726726
httpx_mock: HTTPXMock,
727727
) -> None:
728-
"""Test push when the remote file system was never initialized.
728+
"""Test that a 404 from FileOperations/Structure fails the push.
729729
730-
The backend returns 404 from FileOperations/Structure for projects
731-
whose file system does not exist yet (e.g. a freshly created Function
732-
project). The first push should bootstrap the files instead of failing
733-
the coded-agent validation.
730+
A 404 is not an empty project: Studio Web creates project.uiproj with
731+
every new project, so a missing structure means the project id or the
732+
base URL is wrong (e.g. UIPATH_URL without the organization segment).
733+
It must propagate with the request URL instead of being treated as a
734+
never-pushed project that is allowed through.
734735
"""
735736
base_url = "https://cloud.uipath.com/organization"
736737
project_id = "test-project-id"
738+
structure_url = f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure"
737739

738-
# Uninitialized file system: Structure returns 404
739740
httpx_mock.add_response(
740-
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure",
741+
url=structure_url,
741742
status_code=404,
742743
json={
743744
"type": "https://tools.ietf.org/html/rfc9110#section-15.5.5",
@@ -746,27 +747,6 @@ def test_first_push_to_uninitialized_project(
746747
},
747748
)
748749

749-
self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1)
750-
751-
httpx_mock.add_response(
752-
method="POST",
753-
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration",
754-
status_code=200,
755-
json={"success": True},
756-
)
757-
758-
# Empty folder cleanup - get structure again after migration
759-
httpx_mock.add_response(
760-
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure",
761-
json={
762-
"id": "root",
763-
"name": "root",
764-
"folders": [],
765-
"files": [],
766-
"folderType": "0",
767-
},
768-
)
769-
770750
with runner.isolated_filesystem(temp_dir=temp_dir):
771751
self._create_required_files()
772752

@@ -776,20 +756,15 @@ def test_first_push_to_uninitialized_project(
776756
with open("main.py", "w") as f:
777757
f.write("print('Hello World')")
778758

779-
with open("uv.lock", "w") as f:
780-
f.write('version = 1 \n requires-python = ">=3.11"')
781-
782759
configure_env_vars(mock_env_vars)
783760
os.environ["UIPATH_PROJECT_ID"] = project_id
784761

785762
result = runner.invoke(cli, ["push", "./", "--ignore-resources"])
786-
assert result.exit_code == 0
787-
assert "not of type coded agent" not in result.output
788-
assert "Uploading 'main.py'" in result.output
789-
assert "Uploading 'pyproject.toml'" in result.output
790-
assert "Uploading 'uipath.json'" in result.output
791-
assert "Uploading 'uv.lock'" in result.output
792-
assert "Uploading '.uipath/studio_metadata.json'" in result.output
763+
assert result.exit_code == 1
764+
assert isinstance(result.exception, EnrichedException)
765+
assert result.exception.status_code == 404
766+
assert result.exception.url == structure_url
767+
assert "Uploading" not in result.output
793768

794769
def test_push_to_scaffolded_function_project(
795770
self,

‎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)