mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-09 10:17:00 +00:00
ci(infra): port four CI governance workflows (#37511)
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).
This commit is contained in:
118
.github/scripts/check_extras_sync.py
vendored
Normal file
118
.github/scripts/check_extras_sync.py
vendored
Normal file
@@ -0,0 +1,118 @@
|
||||
"""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))
|
||||
Reference in New Issue
Block a user