Skip to content

Commit f484891

Browse files
committed
Merge branch 'main' of https://github.com/UiPath/uipath-python into josh/cas-interrupt
2 parents 03421f8 + 5022b26 commit f484891

File tree

804 files changed

+65153
-1293
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

804 files changed

+65153
-1293
lines changed

.github/labeler.yml

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
test:uipath-langchain:
2+
- changed-files:
3+
- any-glob-to-any-file: ['packages/uipath/src/**/*.py']
4+
- changed-files:
5+
- any-glob-to-any-file: ['packages/uipath-platform/src/**/*.py']
6+
- changed-files:
7+
- any-glob-to-any-file: ['packages/uipath-core/src/**/*.py']
8+
19
test:uipath-llamaindex:
210
- changed-files:
3-
- any-glob-to-any-file: ['src/**/*.py']
11+
- any-glob-to-any-file: ['packages/uipath/src/**/*.py']
412

5-
test:uipath-langchain:
13+
test:uipath-runtime:
614
- changed-files:
7-
- any-glob-to-any-file: ['src/**/*.py']
15+
- any-glob-to-any-file: ['packages/uipath-core/src/**/*.py']
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env python3
2+
"""Detect which packages have changed in a PR or push to main.
3+
4+
Includes dependency-aware propagation: when a package changes, all
5+
downstream dependents are also included in the test list.
6+
"""
7+
8+
import json
9+
import os
10+
import subprocess
11+
import sys
12+
from pathlib import Path
13+
14+
# Internal dependency graph: package -> packages that depend on it.
15+
# When a package changes, its dependents' tests also run.
16+
# Add new entries here as packages are added to the monorepo.
17+
# External dependents (uipath-langchain, uipath-runtime, etc.) are
18+
# handled separately via labeler.yml auto-labels.
19+
DEPENDENTS: dict[str, list[str]] = {
20+
"uipath-core": ["uipath-platform", "uipath"],
21+
"uipath-platform": ["uipath"],
22+
}
23+
24+
25+
def expand_with_dependents(changed: list[str], all_packages: list[str]) -> list[str]:
26+
"""Expand changed package list to include downstream dependents."""
27+
expanded = set(changed)
28+
for pkg in changed:
29+
for dep in DEPENDENTS.get(pkg, []):
30+
if dep in all_packages:
31+
expanded.add(dep)
32+
return sorted(expanded)
33+
34+
35+
def get_all_packages() -> list[str]:
36+
"""Get all packages in the monorepo."""
37+
packages_dir = Path("packages")
38+
packages = []
39+
40+
for item in packages_dir.iterdir():
41+
if item.is_dir() and (item / "pyproject.toml").exists():
42+
packages.append(item.name)
43+
44+
return sorted(packages)
45+
46+
47+
def get_changed_packages(base_sha: str, head_sha: str) -> list[str]:
48+
"""Get packages that have changed between two commits."""
49+
try:
50+
# Get changed files
51+
result = subprocess.run(
52+
["git", "diff", "--name-only", f"{base_sha}...{head_sha}"],
53+
capture_output=True,
54+
text=True,
55+
check=True,
56+
)
57+
58+
changed_files = result.stdout.strip().split("\n")
59+
60+
# Extract package names from paths like "packages/uipath-llamaindex/..."
61+
changed_packages = set()
62+
for file_path in changed_files:
63+
if file_path.startswith("packages/"):
64+
parts = file_path.split("/")
65+
if len(parts) >= 2:
66+
package_name = parts[1]
67+
# Verify it's a real package
68+
if (Path("packages") / package_name / "pyproject.toml").exists():
69+
changed_packages.add(package_name)
70+
71+
return sorted(changed_packages)
72+
73+
except subprocess.CalledProcessError as e:
74+
print(f"Error running git diff: {e}", file=sys.stderr)
75+
return []
76+
77+
78+
def get_changed_packages_auto() -> list[str]:
79+
"""Auto-detect changed packages using git."""
80+
try:
81+
# Try to detect changes against origin/main
82+
result = subprocess.run(
83+
["git", "diff", "--name-only", "origin/main...HEAD"],
84+
capture_output=True,
85+
text=True,
86+
check=True,
87+
)
88+
89+
changed_files = result.stdout.strip().split("\n")
90+
91+
# Extract package names
92+
changed_packages = set()
93+
for file_path in changed_files:
94+
if file_path.startswith("packages/"):
95+
parts = file_path.split("/")
96+
if len(parts) >= 2:
97+
package_name = parts[1]
98+
if (Path("packages") / package_name / "pyproject.toml").exists():
99+
changed_packages.add(package_name)
100+
101+
return sorted(changed_packages)
102+
103+
except (subprocess.CalledProcessError, Exception) as e:
104+
print(f"Warning: Could not auto-detect changes: {e}", file=sys.stderr)
105+
return []
106+
107+
108+
def main():
109+
"""Main entry point."""
110+
event_name = os.getenv("GITHUB_EVENT_NAME", "")
111+
base_sha = os.getenv("BASE_SHA", "")
112+
head_sha = os.getenv("HEAD_SHA", "")
113+
114+
all_packages = get_all_packages()
115+
116+
# If we have explicit SHAs (from PR or push), detect changed packages
117+
if base_sha and head_sha:
118+
packages = get_changed_packages(base_sha, head_sha)
119+
event_type = "pull request" if event_name == "pull_request" else "push"
120+
print(f"{event_type.capitalize()} - detected {len(packages)} directly changed package(s):")
121+
for pkg in packages:
122+
print(f" - {pkg}")
123+
124+
# workflow_call or missing context - try auto-detection
125+
else:
126+
print(f"Event: {event_name or 'workflow_call'} - attempting auto-detection")
127+
packages = get_changed_packages_auto()
128+
129+
if packages:
130+
print(f"Auto-detected {len(packages)} directly changed package(s):")
131+
for pkg in packages:
132+
print(f" - {pkg}")
133+
else:
134+
# Fallback: test all packages
135+
print("Could not detect changes - testing all packages")
136+
packages = all_packages
137+
for pkg in packages:
138+
print(f" - {pkg}")
139+
140+
# Expand with downstream dependents
141+
expanded = expand_with_dependents(packages, all_packages)
142+
added = sorted(set(expanded) - set(packages))
143+
if added:
144+
print(f"\nAdded {len(added)} dependent package(s):")
145+
for pkg in added:
146+
print(f" - {pkg}")
147+
packages = expanded
148+
149+
# Output as JSON for GitHub Actions
150+
packages_json = json.dumps(packages)
151+
print(f"\nPackages JSON: {packages_json}")
152+
153+
# Write to GitHub output
154+
github_output = os.getenv("GITHUB_OUTPUT")
155+
if github_output:
156+
with open(github_output, "a") as f:
157+
f.write(f"packages={packages_json}\n")
158+
f.write(f"count={len(packages)}\n")
159+
160+
161+
if __name__ == "__main__":
162+
main()
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#!/usr/bin/env python3
2+
"""Detect which packages need publishing to PyPI.
3+
4+
Compares local package versions against what's already on PyPI.
5+
Only outputs packages whose local version doesn't exist on PyPI yet.
6+
"""
7+
8+
import json
9+
import os
10+
import sys
11+
import urllib.request
12+
import urllib.error
13+
from pathlib import Path
14+
15+
try:
16+
import tomllib
17+
except ModuleNotFoundError:
18+
import tomli as tomllib # type: ignore[no-redef]
19+
20+
21+
def get_all_packages() -> list[dict[str, str]]:
22+
"""Get all packages with their names and versions from pyproject.toml."""
23+
packages_dir = Path("packages")
24+
packages = []
25+
26+
for item in sorted(packages_dir.iterdir()):
27+
pyproject = item / "pyproject.toml"
28+
if item.is_dir() and pyproject.exists():
29+
with open(pyproject, "rb") as f:
30+
data = tomllib.load(f)
31+
project = data.get("project", {})
32+
name = project.get("name")
33+
version = project.get("version")
34+
if name and version:
35+
packages.append(
36+
{"directory": item.name, "name": name, "version": version}
37+
)
38+
39+
return packages
40+
41+
42+
def version_exists_on_pypi(package_name: str, version: str) -> bool:
43+
"""Check if a specific version of a package exists on PyPI."""
44+
url = f"https://pypi.org/pypi/{package_name}/{version}/json"
45+
try:
46+
req = urllib.request.Request(url, method="HEAD")
47+
with urllib.request.urlopen(req, timeout=10):
48+
return True
49+
except urllib.error.HTTPError as e:
50+
if e.code == 404:
51+
return False
52+
print(
53+
f" Warning: PyPI returned HTTP {e.code} for {package_name}=={version}",
54+
file=sys.stderr,
55+
)
56+
return False
57+
except Exception as e:
58+
print(
59+
f" Warning: Could not check PyPI for {package_name}=={version}: {e}",
60+
file=sys.stderr,
61+
)
62+
# If we can't reach PyPI, skip publishing to be safe
63+
return True
64+
65+
66+
def main():
67+
"""Main entry point."""
68+
all_packages = get_all_packages()
69+
publishable = []
70+
71+
print(f"Checking {len(all_packages)} package(s) against PyPI...")
72+
73+
for pkg in all_packages:
74+
name, version, directory = pkg["name"], pkg["version"], pkg["directory"]
75+
exists = version_exists_on_pypi(name, version)
76+
if exists:
77+
print(f" {name}=={version} — already on PyPI, skipping")
78+
else:
79+
print(f" {name}=={version} — new version, will publish")
80+
publishable.append(directory)
81+
82+
# Output as JSON for GitHub Actions
83+
packages_json = json.dumps(publishable)
84+
print(f"\nPublishable packages JSON: {packages_json}")
85+
86+
github_output = os.getenv("GITHUB_OUTPUT")
87+
if github_output:
88+
with open(github_output, "a") as f:
89+
f.write(f"packages={packages_json}\n")
90+
f.write(f"count={len(publishable)}\n")
91+
92+
93+
if __name__ == "__main__":
94+
main()

0 commit comments

Comments
 (0)