mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
Four GitHub Actions workflows ported from the Deep Agents monorepo to enforce repository hygiene rules that were not previously applied here. ## Changes - **Fork-main PR guard**: closes PRs from forks whose head is `main` or `master`, with a sticky comment explaining how to reopen from a feature branch. Prevents the "Update branch" → admin-override path that lets a `Merge branch 'master' into master` commit land on the default branch and bypass squash-only policy. Maintainers can override with a `bypass-fork-main-check` label. - **Monthly uv pin bump**: opens a PR on the first of each month to advance `UV_VERSION` in the composite setup action. Probes `releases.astral.sh` across four architectures before committing so CI doesn't race a lagging mirror on fresh-release days — the gap Dependabot's `github-actions` ecosystem can't cover because it tracks `uses:` SHA pins, not the inline `UV_VERSION` value. - **Extras-sync validation**: a Python script (`check_extras_sync.py`) and companion workflow that detect version-constraint drift between `[project.dependencies]` and `[project.optional-dependencies]` across every `libs/**/pyproject.toml`. Runs on PRs touching any `pyproject.toml` and on pushes to `master`; is a no-op on packages that declare no extras. - **Banned-trailer pre-merge lint**: rejects PR descriptions containing a `Co-authored-by: ... <noreply@anthropic.com>` trailer before the PR reaches merge, where the org ruleset would reject the squash-push anyway. Posts a sticky comment with remediation steps; updates it to a "resolved" state when the trailer is removed, rather than deleting (which requires elevated token scope on fork PRs).
119 lines
3.9 KiB
Python
119 lines
3.9 KiB
Python
"""Check that optional extras stay in sync with required dependencies.
|
|
|
|
When a package appears in both [project.dependencies] and
|
|
[project.optional-dependencies], we ensure their version constraints match.
|
|
This prevents silent version drift (e.g. bumping a required dep but
|
|
forgetting the corresponding extra).
|
|
"""
|
|
|
|
import sys
|
|
import tomllib
|
|
from pathlib import Path
|
|
from re import compile as re_compile
|
|
|
|
# Matches the package name at the start of a PEP 508 dependency string.
|
|
# Stops at the first non-name character; downstream code is responsible for
|
|
# stripping extras (`[...]`) and env markers (`; ...`) from the remainder.
|
|
_NAME_RE = re_compile(r"^([A-Za-z0-9]([A-Za-z0-9._-]*[A-Za-z0-9])?)")
|
|
|
|
|
|
def _normalize(name: str) -> str:
|
|
"""Normalize a package name for equality comparison.
|
|
|
|
Lowercases and maps `-` and `.` to `_`. Looser than PEP 503
|
|
(which uses `-` and collapses runs), but sufficient for matching the
|
|
same package across two PEP 508 strings.
|
|
|
|
Returns:
|
|
Lowercased, underscore-normalized package name.
|
|
"""
|
|
return name.lower().replace("-", "_").replace(".", "_")
|
|
|
|
|
|
def _parse_dep(dep: str) -> tuple[str, str]:
|
|
"""Return `(normalized_name, version_spec)` from a PEP 508 string.
|
|
|
|
Strips extras (`pkg[async]`), environment markers (`; python_version ...`),
|
|
URL specifiers (`pkg @ git+...`), and whitespace so the returned
|
|
`version_spec` is directly comparable between a required and optional dep.
|
|
|
|
Returns:
|
|
Tuple of normalized package name and bare version specifier.
|
|
|
|
Raises:
|
|
ValueError: If the dependency string cannot be parsed.
|
|
"""
|
|
match = _NAME_RE.match(dep)
|
|
if not match:
|
|
msg = f"Cannot parse dependency: {dep!r}"
|
|
raise ValueError(msg)
|
|
name = match.group(1)
|
|
rest = dep[match.end() :].strip()
|
|
|
|
if rest.startswith("["):
|
|
close = rest.find("]")
|
|
if close == -1:
|
|
msg = f"Unclosed extras bracket in dependency: {dep!r}"
|
|
raise ValueError(msg)
|
|
rest = rest[close + 1 :].strip()
|
|
|
|
if ";" in rest:
|
|
rest = rest.split(";", 1)[0].strip()
|
|
|
|
# URL specifiers have no comparable version; treat as unconstrained.
|
|
if rest.startswith("@"):
|
|
rest = ""
|
|
|
|
rest = " ".join(rest.split())
|
|
return _normalize(name), rest
|
|
|
|
|
|
def main(pyproject_path: Path) -> int:
|
|
"""Check extras sync and return `0` on pass, `1` on mismatch or parse error."""
|
|
with pyproject_path.open("rb") as f:
|
|
data = tomllib.load(f)
|
|
|
|
required: dict[str, str] = {}
|
|
for dep in data.get("project", {}).get("dependencies", []):
|
|
try:
|
|
name, spec = _parse_dep(dep)
|
|
except ValueError as e:
|
|
print(f"::error file={pyproject_path}::{e}")
|
|
return 1
|
|
required[name] = spec
|
|
|
|
optional = data.get("project", {}).get("optional-dependencies", {})
|
|
if not optional:
|
|
return 0
|
|
|
|
mismatches: list[str] = []
|
|
for group, deps in optional.items():
|
|
for dep in deps:
|
|
try:
|
|
name, spec = _parse_dep(dep)
|
|
except ValueError as e:
|
|
print(f"::error file={pyproject_path}::{e}")
|
|
return 1
|
|
if name in required and spec != required[name]:
|
|
mismatches.append(
|
|
f" [{group}] {name}: extra has '{spec}' "
|
|
f"but required dep has '{required[name]}'"
|
|
)
|
|
|
|
if mismatches:
|
|
print(f"Extra / required dependency version mismatch in {pyproject_path}:")
|
|
print("\n".join(mismatches))
|
|
print(
|
|
"\nUpdate the optional extras in [project.optional-dependencies] "
|
|
"to match [project.dependencies]."
|
|
)
|
|
return 1
|
|
|
|
print(f"All extras in {pyproject_path} are in sync with required dependencies.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
path = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("pyproject.toml")
|
|
raise SystemExit(main(path))
|