mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-08-27 10:51:26 -04:00
ed0b6785f8
Adds a workflow that raises LangChain-ecosystem dependency lower bounds to the latest stable PyPI release and opens a `chore(deps):` PR with the result. Runs weekly (Monday 09:00 UTC) across every release package, and can be dispatched manually against a single package or `all`. Keeping integration minimums current is a recurring manual chore. This automates the "bump the floor" half: for each in-scope requirement (`langchain*`, `langgraph*`, `langsmith*`, `deepagents*`) declaring a `>=`/`~=` floor in `[project.dependencies]`, `[project.optional-dependencies]`, or `[dependency-groups]`, the floor is rewritten in place and one PR is opened. - **Raises within the existing range, never past an upper bound.** `deepagents>=0.7.0,<0.8.0` becomes `>=0.7.9,<0.8.0`, never the unsatisfiable `>=0.8.0,<0.8.0`. Pre-releases are never selected, and a floor already ahead of the latest stable release (intentional prerelease coordination) is left alone. Each rewrite is checked against the versions PyPI actually offers, so a bump can't quietly exclude a version the old range allowed. - **Preserves everything else in the specifier.** Upper bounds, extras, and markers survive verbatim, and `~=` ceilings are held by keeping the original component count — `~=1.2` becomes `~=1.9`, not `~=1.9.4`. - **Skips what it shouldn't touch.** Exact `==` pins, specs with no floor, URL requirements, workspace-local `[tool.uv.sources]` deps, and the manifest's own package name. - **Regenerates every `uv.lock` the edits invalidate** — not only the edited packages: a lockfile embeds the specifiers of anything it resolves from a local path source, so raising a floor in `libs/deepagents` stales `libs/evals` and friends. Interpreter versions come from `check_lockfiles_pre_commit` so they match what `check_lockfiles.yml` verifies. - **Reports failure instead of silence.** An unresolvable PyPI lookup or an unrewritable manifest is listed in the PR body under "Not raised" and fails the run, so an unattended cron never renders an outage as "everything is already up to date". - **Idempotent per package.** If a PR is already open for the selected package, or nothing needs raising, the run exits without creating a duplicate. PRs are created with the Org Membership App token so required `pull_request` checks trigger, and titled `chore(deps):` so release-please doesn't fan out a separate release PR.
103 lines
3.1 KiB
Python
103 lines
3.1 KiB
Python
"""Run lockfile checks only for packages touched by changed paths."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shlex
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[3]
|
|
LIBS_ROOT = REPO_ROOT / "libs"
|
|
EXAMPLES_ROOT = REPO_ROOT / "examples"
|
|
|
|
|
|
def package_dirs() -> list[Path]:
|
|
"""Return every package directory that owns a `uv.lock`, in repo-path order."""
|
|
libs = [path.parent for path in LIBS_ROOT.glob("*/Makefile")]
|
|
partners = [path.parent for path in (LIBS_ROOT / "partners").glob("*/Makefile")]
|
|
examples = [path.parent for path in EXAMPLES_ROOT.glob("*/pyproject.toml")]
|
|
return sorted([*libs, *partners, *examples], key=_repo_path)
|
|
|
|
|
|
def _repo_path(path: Path) -> str:
|
|
return path.relative_to(REPO_ROOT).as_posix()
|
|
|
|
|
|
def python_version(package: Path) -> str:
|
|
"""Return the interpreter version `package`'s lockfile must be resolved with."""
|
|
if package == LIBS_ROOT / "acp":
|
|
return "3.14"
|
|
return "3.12"
|
|
|
|
|
|
def _label(package: Path) -> str:
|
|
try:
|
|
return package.relative_to(LIBS_ROOT).as_posix()
|
|
except ValueError:
|
|
return f"../{package.relative_to(REPO_ROOT).as_posix()}"
|
|
|
|
|
|
def _touches_package(path: str, package: Path) -> bool:
|
|
package_path = _repo_path(package)
|
|
return path == package_path or path.startswith(f"{package_path}/")
|
|
|
|
|
|
def _packages_for_paths(paths: list[str]) -> list[Path]:
|
|
packages = package_dirs()
|
|
if not paths:
|
|
return packages
|
|
return [
|
|
package
|
|
for package in packages
|
|
if any(_touches_package(path, package) for path in paths)
|
|
]
|
|
|
|
|
|
def _lock_command(package: Path, *, check: bool) -> list[str]:
|
|
command = ["uv", "lock"]
|
|
if check:
|
|
command.append("--check")
|
|
return [
|
|
*command,
|
|
"--directory",
|
|
package.relative_to(REPO_ROOT).as_posix(),
|
|
"--python",
|
|
python_version(package),
|
|
]
|
|
|
|
|
|
def _lockfile_error(package: Path) -> str:
|
|
package_path = package.relative_to(REPO_ROOT).as_posix()
|
|
lockfile = f"{package_path}/uv.lock"
|
|
command = shlex.join(_lock_command(package, check=False))
|
|
return (
|
|
f"::error file={lockfile},title=Out-of-date uv.lock::"
|
|
f"{lockfile} is out of sync with {package_path}/pyproject.toml. "
|
|
f"From the repository root, run `{command}` and commit the updated lockfile."
|
|
)
|
|
|
|
|
|
def main(paths: list[str]) -> int:
|
|
"""Check lockfiles for packages touched by `paths`, or every package if empty."""
|
|
packages = _packages_for_paths(paths)
|
|
if not packages:
|
|
print("✅ No package lockfiles need checking.")
|
|
return 0
|
|
for package in packages:
|
|
print(f"🔍 Checking {_label(package)}")
|
|
result = subprocess.run(
|
|
_lock_command(package, check=True),
|
|
check=False,
|
|
cwd=REPO_ROOT,
|
|
)
|
|
if result.returncode != 0:
|
|
print(_lockfile_error(package), file=sys.stderr)
|
|
return result.returncode
|
|
print("✅ All applicable lockfiles are up-to-date!")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|