mirror of
https://github.com/hwchase17/langchain.git
synced 2026-06-30 22:26:49 +00:00
ci(infra): check release dependency pins against PyPI (#38048)
Release PRs now get an earlier dependency-resolution check before merge, catching unpublished intra-monorepo pins before they can produce broken wheel metadata. The minimum-version helper also fails with a clear error when no published PyPI version satisfies a declared constraint, instead of emitting an invalid `pkg==None` requirement.
This commit is contained in:
16
.github/scripts/get_min_versions.py
vendored
16
.github/scripts/get_min_versions.py
vendored
@@ -196,4 +196,20 @@ if __name__ == "__main__":
|
||||
# Call the function to get the minimum versions
|
||||
min_versions = get_min_version_from_toml(toml_file, versions_for, python_version)
|
||||
|
||||
# A `None` value means no *published* version on PyPI satisfies the declared
|
||||
# constraint, e.g. a `release(...)` PR bumped a minimum pin to a version that
|
||||
# has not shipped yet. Emitting `pkg==None` would be passed verbatim to
|
||||
# `uv pip install` in the release workflow's minimum-version test step,
|
||||
# producing a cryptic install failure, so fail loudly here instead.
|
||||
unresolved = [lib for lib, version in min_versions.items() if version is None]
|
||||
if unresolved:
|
||||
print(
|
||||
"ERROR: no published version on PyPI satisfies the declared constraint "
|
||||
f"for: {', '.join(sorted(unresolved))}. A release likely pinned a "
|
||||
"dependency to a version that is not yet published. Release the "
|
||||
"dependency first, or relax the pin.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
print(" ".join([f"{lib}=={version}" for lib, version in min_versions.items()]))
|
||||
|
||||
114
.github/workflows/check_release_deps.yml
vendored
Normal file
114
.github/workflows/check_release_deps.yml
vendored
Normal file
@@ -0,0 +1,114 @@
|
||||
# Validate that a release PR's declared dependencies are actually published on
|
||||
# PyPI *before* the package itself is released.
|
||||
#
|
||||
# WHY: `release(scope): x.y.z` PRs frequently bump intra-monorepo minimum pins
|
||||
# (e.g. `langchain-core>=1.4.4`). The regular PR test suite deliberately SKIPS
|
||||
# minimum-version resolution for langchain-core / langchain / langchain-text-splitters
|
||||
# (see `SKIP_IF_PULL_REQUEST` in `.github/scripts/get_min_versions.py`) because normal
|
||||
# feature PRs may bump those in lockstep with an as-yet-unpublished sibling release.
|
||||
#
|
||||
# For a `release` PR, though, every runtime dependency *should* already be on PyPI —
|
||||
# that is the convention (release `langchain-core` first, then downstream packages).
|
||||
# If a pin points at a version that does not exist yet, the published wheel's metadata
|
||||
# is unresolvable and `pip install <pkg>==x.y.z` breaks for end users. Without this
|
||||
# workflow, that is only caught at release-trigger time, when `get_min_versions.py`
|
||||
# resolves the pins against PyPI (its companion change in this PR now exits loudly on
|
||||
# an unpublished pin instead of emitting `pkg==None`). This workflow adds a second,
|
||||
# earlier guard: it shifts the same check left onto the release PR, so the author
|
||||
# finds out before merge rather than when the release job runs.
|
||||
#
|
||||
# HOW: resolve each changed package's runtime dependencies against real PyPI with
|
||||
# `uv pip compile --no-sources`, which ignores the editable `[tool.uv.sources]`
|
||||
# workspace overrides so intra-monorepo deps resolve from the index exactly as an
|
||||
# end user's installer would see them. This resolves from package index metadata and
|
||||
# does not build or run the PR's own project code.
|
||||
|
||||
name: "🚀 Check Release Dependencies"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
paths:
|
||||
- "libs/**/pyproject.toml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
check-release-deps:
|
||||
name: "✅ Verify release dependencies exist on PyPI"
|
||||
# Only run for release PRs (`release(scope): x.y.z`). Other PRs may bump
|
||||
# intra-monorepo pins ahead of a sibling release on purpose.
|
||||
if: startsWith(github.event.pull_request.title, 'release')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "🐍 Set up Python + uv"
|
||||
uses: "./.github/actions/uv_setup"
|
||||
with:
|
||||
python-version: "3.12"
|
||||
enable-cache: "false"
|
||||
|
||||
- name: "🔍 Resolve runtime dependencies against PyPI"
|
||||
shell: bash
|
||||
env:
|
||||
# Passed via env (not inline interpolation) to keep PR-controlled
|
||||
# values out of the shell command string.
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# pyproject.toml manifests changed by this PR.
|
||||
mapfile -t changed < <(
|
||||
git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'libs/**/pyproject.toml'
|
||||
)
|
||||
|
||||
if [ "${#changed[@]}" -eq 0 ]; then
|
||||
# The `paths:` filter should prevent this, so an empty list is
|
||||
# surprising — surface it loudly rather than passing silently.
|
||||
echo "::notice::No libs/**/pyproject.toml changed in this PR; nothing to validate."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
failed=0
|
||||
for manifest in "${changed[@]}"; do
|
||||
pkg_dir="$(dirname "$manifest")"
|
||||
echo "::group::Resolving ${manifest} against PyPI"
|
||||
# --no-sources ignores [tool.uv.sources] editable workspace overrides,
|
||||
# so intra-monorepo deps resolve from PyPI like an end-user install.
|
||||
# --universal resolves across the full requires-python range, so deps
|
||||
# gated behind Python-version markers are validated too.
|
||||
if uv pip compile --no-sources --universal "$manifest" > /dev/null; then
|
||||
echo "✅ ${pkg_dir}: all runtime dependencies resolve on PyPI"
|
||||
else
|
||||
echo "❌ ${pkg_dir}: a dependency pin is not satisfiable on PyPI"
|
||||
failed=1
|
||||
fi
|
||||
echo "::endgroup::"
|
||||
done
|
||||
|
||||
if [ "$failed" -ne 0 ]; then
|
||||
cat >&2 <<'EOF'
|
||||
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ One or more dependency pins could not be resolved from PyPI. │
|
||||
│ See the per-package resolver output above for the exact reason. │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
|
||||
Most likely, a `release(scope): x.y.z` PR pinned a dependency to a
|
||||
version that is not yet published on PyPI. The released wheel's metadata
|
||||
would then be unresolvable, breaking `pip install` for end users. Fix by:
|
||||
• Releasing the dependency package first so the pinned version exists
|
||||
on PyPI, then re-running this check; or
|
||||
• Relaxing the version pin to a published version.
|
||||
|
||||
If the resolver output above shows a network/index error (rather than
|
||||
"No solution found"), this may be a transient PyPI issue — re-run the job.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
Reference in New Issue
Block a user