Skip to content

Commit 0984276

Browse files
committed
Revert "feat(cli): ship files directly in evals/ too"
This reverts commit b1890f1. Files directly in the root `evals` folder break Studio Web's project migration ("Project storage is corrupted", SRE-522088), which is why `uipath push` skips them (#1356). A packed project can be imported into Studio Web too, so `uipath pack` keeps skipping them as well. Everything under `evals/` subfolders, where Studio Web writes its datasets, still ships. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LCRTCKqfp7xdLeHQZ6MGUy
1 parent b1890f1 commit 0984276

6 files changed

Lines changed: 211 additions & 23 deletions

File tree

‎packages/uipath/src/uipath/_cli/_push/sw_file_handler.py‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -534,12 +534,25 @@ async def upload_source_files(
534534
remote_files = self._get_remote_files(structure)
535535

536536
# Get files to upload and process them
537-
local_files = files_to_include(
537+
local_files, skipped_files = files_to_include(
538538
pack_options,
539539
self.directory,
540540
self.include_uv_lock,
541541
)
542542

543+
# Log skipped files from root evals folder
544+
if skipped_files:
545+
evals_folder_path = os.path.join(self.directory, LEGACY_EVAL_FOLDER)
546+
logger.info(
547+
f"Skipping {len(skipped_files)} file(s) in evals folder ({evals_folder_path}): {', '.join(skipped_files)}"
548+
)
549+
for skipped_file in skipped_files:
550+
yield UpdateEvent(
551+
file_path=skipped_file,
552+
status="skipped",
553+
message=f"Skipping '{skipped_file}' (evals folder)",
554+
)
555+
543556
updates = await self._process_file_uploads(local_files, remote_files)
544557

545558
# Yield all updates

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

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from uipath._cli.models.uipath_json_schema import PackOptions, UiPathJsonConfig
1515
from uipath.platform.common import UiPathConfig
1616
from uipath.platform.constants import (
17+
LEGACY_EVAL_FOLDER,
1718
PYTHON_CONFIGURATION_FILE,
1819
UIPATH_CONFIG_FILE,
1920
)
@@ -406,7 +407,7 @@ def files_to_include(
406407
directory: str,
407408
include_uv_lock: bool = True,
408409
directories_to_ignore: list[str] | None = None,
409-
) -> list[FileInfo]:
410+
) -> tuple[list[FileInfo], list[str]]:
410411
"""Get list of files to include in the project based on configuration.
411412
412413
Walks through the directory tree and identifies files to include based on extensions
@@ -419,7 +420,7 @@ def files_to_include(
419420
directories_to_ignore: List of directories to ignore
420421
421422
Returns:
422-
list[FileInfo]: Files to include
423+
tuple[list[FileInfo], list[str]]: Tuple of (included files, skipped file paths)
423424
"""
424425
file_extensions_included = [".py", ".mermaid", ".json", ".yaml", ".yml", ".md"]
425426
files_included = [PYTHON_CONFIGURATION_FILE]
@@ -455,9 +456,15 @@ def is_venv_dir(d: str) -> bool:
455456
)
456457

457458
extra_files: list[FileInfo] = []
459+
skipped_files: list[str] = []
458460

459461
# Walk through directory and return all files in the allowlist
460462
for root, dirs, files in os.walk(directory):
463+
# Determine if we're in the root evals folder
464+
root_rel_path = os.path.relpath(root, directory)
465+
normalized_root_rel_path = root_rel_path.replace(os.sep, "/")
466+
is_root_evals_folder = normalized_root_rel_path == LEGACY_EVAL_FOLDER
467+
461468
# Skip all directories that start with . or are a venv or are excluded
462469
included_dirs = []
463470
for d in dirs:
@@ -498,6 +505,11 @@ def is_venv_dir(d: str) -> bool:
498505
# Normalize the path
499506
normalized_rel_path = rel_path.replace(os.sep, "/")
500507

508+
# Skip files in the root evals folder (but allow eval-set and evaluators subdirectories)
509+
if is_root_evals_folder:
510+
skipped_files.append(normalized_rel_path)
511+
continue
512+
501513
# Check inclusion: by extension, by filename (for base directory), or by relative path
502514
should_include = (
503515
file_extension in file_extensions_included
@@ -526,7 +538,7 @@ def is_venv_dir(d: str) -> bool:
526538
is_binary=is_binary_file(file_extension),
527539
)
528540
)
529-
return extra_files
541+
return extra_files, skipped_files
530542

531543

532544
def compute_normalized_hash(content: str) -> str:

‎packages/uipath/src/uipath/_cli/cli_pack.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,9 +319,9 @@ def pack_fn(
319319
metadata_files["_rels/.rels"] = rels_content
320320

321321
# Studio Web's Evaluations panel stores a project's datasets under `evals/`,
322-
# so that whole folder ships with the package, as `uip solution pack` does.
323-
# `evaluations/`, the folder `uipath eval` reads, stays out of it.
324-
files = files_to_include(
322+
# so that folder ships with the package. `evaluations/`, the folder
323+
# `uipath eval` reads, stays out of it.
324+
files, skipped_files = files_to_include(
325325
config_data.pack_options,
326326
directory,
327327
include_uv_lock,

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def test_hidden_files_are_excluded(self, tmp_path):
1010
open(os.path.join(project_dir, ".hidden_file.py"), "w").close()
1111
open(os.path.join(project_dir, ".env"), "w").close()
1212

13-
included = files_to_include(None, project_dir, include_uv_lock=False)
13+
included, _ = files_to_include(None, project_dir, include_uv_lock=False)
1414
included_names = [f.file_name for f in included]
1515

1616
assert "main.py" in included_names
@@ -24,7 +24,7 @@ def test_hidden_files_in_subdirectory_are_excluded(self, tmp_path):
2424
open(os.path.join(sub_dir, "app.py"), "w").close()
2525
open(os.path.join(sub_dir, ".secret.json"), "w").close()
2626

27-
included = files_to_include(None, project_dir, include_uv_lock=False)
27+
included, _ = files_to_include(None, project_dir, include_uv_lock=False)
2828
included_names = [f.file_name for f in included]
2929

3030
assert "app.py" in included_names

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

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,13 +1106,14 @@ def test_evals_directory_ships_and_evaluations_directory_excluded(
11061106
# the dataset would change these bytes.
11071107
eval_set_json = '{\n "name": "smoke",\n "rows": [{ "x": 1 }]\n}\n'
11081108

1109-
# Where Studio Web's Evaluations panel writes datasets: ships, files
1110-
# directly in evals/ included.
1109+
# Where Studio Web's Evaluations panel writes datasets: ships.
11111110
os.makedirs("evals/default/eval-sets")
11121111
with open("evals/default/eval-sets/x.json", "w") as f:
11131112
f.write(eval_set_json)
1114-
with open("evals/root.json", "w") as f:
1115-
f.write('{"at": "root"}')
1113+
# A stray file directly in evals/ breaks Studio Web's project
1114+
# migration (SRE-522088), so it stays out, as it does on push.
1115+
with open("evals/stray.json", "w") as f:
1116+
f.write("{}")
11161117
# The folder `uipath eval` reads, and local-only state: none of it ships.
11171118
os.makedirs("evaluations/eval-sets")
11181119
with open("evaluations/eval-sets/legacy.json", "w") as f:
@@ -1146,7 +1147,7 @@ def test_evals_directory_ships_and_evaluations_directory_excluded(
11461147
z.read("content/evals/default/eval-sets/x.json").decode("utf-8")
11471148
== eval_set_json
11481149
)
1149-
assert "content/evals/root.json" in z.namelist()
1150+
assert "content/evals/stray.json" not in z.namelist()
11501151
assert "content/other_dir/included.json" in z.namelist()
11511152

11521153
excluded_folders = (

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

Lines changed: 171 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,15 +1630,15 @@ def test_push_shows_up_to_date_for_unchanged_files(
16301630
assert "Updating 'main.py'" not in result.output
16311631
assert "Updating 'helper.py'" not in result.output
16321632

1633-
def test_push_uploads_root_evals_folder_files(
1633+
def test_push_skips_root_evals_folder_files(
16341634
self,
16351635
runner: CliRunner,
16361636
temp_dir: str,
16371637
project_details: ProjectDetails,
16381638
mock_env_vars: dict[str, str],
16391639
httpx_mock: HTTPXMock,
16401640
) -> None:
1641-
"""Test that files directly in the evals folder are uploaded during push."""
1641+
"""Test that files in root evals folder are skipped during push."""
16421642
base_url = "https://cloud.uipath.com/organization"
16431643
project_id = "test-project-id"
16441644

@@ -1693,11 +1693,11 @@ def test_push_uploads_root_evals_folder_files(
16931693
os.makedirs("evals/eval-sets")
16941694
os.makedirs("evals/evaluators")
16951695

1696-
# Create files in root evals folder (should be included)
1696+
# Create files in root evals folder (should be skipped)
16971697
with open("evals/root_file1.json", "w") as f:
1698-
f.write('{"should": "be included"}')
1698+
f.write('{"should": "be skipped"}')
16991699
with open("evals/root_file2.py", "w") as f:
1700-
f.write("print('should be included')")
1700+
f.write("print('should be skipped')")
17011701

17021702
# Create files in eval-sets (should be included)
17031703
with open("evals/eval-sets/test_eval.json", "w") as f:
@@ -1713,10 +1713,9 @@ def test_push_uploads_root_evals_folder_files(
17131713
result = runner.invoke(cli, ["push", "./", "--ignore-resources"])
17141714
assert result.exit_code == 0
17151715

1716-
# Verify root evals files are uploaded
1717-
assert "Uploading 'root_file1.json'" in result.output
1718-
assert "Uploading 'root_file2.py'" in result.output
1719-
assert "Skipping" not in result.output
1716+
# Verify root evals files are skipped
1717+
assert "Skipping 'evals/root_file1.json' (evals folder)" in result.output
1718+
assert "Skipping 'evals/root_file2.py' (evals folder)" in result.output
17201719

17211720
# Verify eval-sets and evaluators files are uploaded
17221721
assert "Uploading 'test_eval.json'" in result.output
@@ -1725,6 +1724,169 @@ def test_push_uploads_root_evals_folder_files(
17251724
# Verify main.py is uploaded
17261725
assert "Uploading 'main.py'" in result.output
17271726

1727+
def test_push_skips_only_root_evals_files_not_subdirectories(
1728+
self,
1729+
runner: CliRunner,
1730+
temp_dir: str,
1731+
project_details: ProjectDetails,
1732+
mock_env_vars: dict[str, str],
1733+
httpx_mock: HTTPXMock,
1734+
) -> None:
1735+
"""Test that only files directly in evals folder are skipped, not files in subdirectories."""
1736+
base_url = "https://cloud.uipath.com/organization"
1737+
project_id = "test-project-id"
1738+
1739+
mock_structure = {
1740+
"id": "root",
1741+
"name": "root",
1742+
"folders": [],
1743+
"files": [
1744+
{
1745+
"id": "123",
1746+
"name": "pyproject.toml",
1747+
"isMain": False,
1748+
"fileType": "1",
1749+
"isEntryPoint": False,
1750+
"ignoredFromPublish": False,
1751+
},
1752+
],
1753+
"folderType": "0",
1754+
}
1755+
1756+
httpx_mock.add_response(
1757+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure",
1758+
json=mock_structure,
1759+
)
1760+
1761+
self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1)
1762+
self._mock_file_download(httpx_mock, "123")
1763+
1764+
httpx_mock.add_response(
1765+
method="POST",
1766+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration",
1767+
status_code=200,
1768+
json={"success": True},
1769+
)
1770+
1771+
httpx_mock.add_response(
1772+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure",
1773+
json=mock_structure,
1774+
)
1775+
1776+
with runner.isolated_filesystem(temp_dir=temp_dir):
1777+
self._create_required_files()
1778+
1779+
with open("pyproject.toml", "w") as f:
1780+
f.write(project_details.to_toml())
1781+
1782+
# Create evals folder with various subdirectories
1783+
os.makedirs("evals/eval-sets")
1784+
os.makedirs("evals/evaluators")
1785+
os.makedirs("evals/custom-subfolder")
1786+
1787+
# Files directly in evals (should be skipped)
1788+
with open("evals/config.json", "w") as f:
1789+
f.write('{"root": "file"}')
1790+
1791+
# Files in subdirectories (should be included)
1792+
with open("evals/eval-sets/test.json", "w") as f:
1793+
f.write('{"eval": "set"}')
1794+
with open("evals/evaluators/eval.py", "w") as f:
1795+
f.write("def evaluate(): pass")
1796+
with open("evals/custom-subfolder/data.json", "w") as f:
1797+
f.write('{"custom": "data"}')
1798+
1799+
configure_env_vars(mock_env_vars)
1800+
os.environ["UIPATH_PROJECT_ID"] = project_id
1801+
1802+
result = runner.invoke(cli, ["push", "./", "--ignore-resources"])
1803+
assert result.exit_code == 0
1804+
1805+
# Verify root file is skipped
1806+
assert "Skipping 'evals/config.json' (evals folder)" in result.output
1807+
1808+
# Verify subdirectory files are included
1809+
assert "Uploading 'test.json'" in result.output
1810+
assert "Uploading 'eval.py'" in result.output
1811+
assert "Uploading 'data.json'" in result.output
1812+
1813+
def test_push_logs_skipped_files_count(
1814+
self,
1815+
runner: CliRunner,
1816+
temp_dir: str,
1817+
project_details: ProjectDetails,
1818+
mock_env_vars: dict[str, str],
1819+
httpx_mock: HTTPXMock,
1820+
) -> None:
1821+
"""Test that push logs the count of skipped files from root evals folder."""
1822+
base_url = "https://cloud.uipath.com/organization"
1823+
project_id = "test-project-id"
1824+
1825+
mock_structure = {
1826+
"id": "root",
1827+
"name": "root",
1828+
"folders": [],
1829+
"files": [
1830+
{
1831+
"id": "123",
1832+
"name": "pyproject.toml",
1833+
"isMain": False,
1834+
"fileType": "1",
1835+
"isEntryPoint": False,
1836+
"ignoredFromPublish": False,
1837+
},
1838+
],
1839+
"folderType": "0",
1840+
}
1841+
1842+
httpx_mock.add_response(
1843+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure",
1844+
json=mock_structure,
1845+
)
1846+
1847+
self._mock_lock_retrieval(httpx_mock, base_url, project_id, times=1)
1848+
self._mock_file_download(httpx_mock, "123")
1849+
1850+
httpx_mock.add_response(
1851+
method="POST",
1852+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/StructuralMigration",
1853+
status_code=200,
1854+
json={"success": True},
1855+
)
1856+
1857+
httpx_mock.add_response(
1858+
url=f"{base_url}/studio_/backend/api/Project/{project_id}/FileOperations/Structure",
1859+
json=mock_structure,
1860+
)
1861+
1862+
with runner.isolated_filesystem(temp_dir=temp_dir):
1863+
self._create_required_files()
1864+
1865+
with open("pyproject.toml", "w") as f:
1866+
f.write(project_details.to_toml())
1867+
1868+
# Create evals folder with multiple files
1869+
os.makedirs("evals")
1870+
1871+
# Create 3 files in root evals
1872+
with open("evals/file1.json", "w") as f:
1873+
f.write("{}")
1874+
with open("evals/file2.py", "w") as f:
1875+
f.write("pass")
1876+
with open("evals/file3.yaml", "w") as f:
1877+
f.write("key: value")
1878+
1879+
configure_env_vars(mock_env_vars)
1880+
os.environ["UIPATH_PROJECT_ID"] = project_id
1881+
1882+
result = runner.invoke(cli, ["push", "./", "--ignore-resources"])
1883+
assert result.exit_code == 0
1884+
1885+
# Verify individual files are skipped
1886+
assert "Skipping 'evals/file1.json' (evals folder)" in result.output
1887+
assert "Skipping 'evals/file2.py' (evals folder)" in result.output
1888+
assert "Skipping 'evals/file3.yaml' (evals folder)" in result.output
1889+
17281890
def test_push_preserves_remote_evals_when_no_local_evals(
17291891
self,
17301892
runner: CliRunner,

0 commit comments

Comments
 (0)