Files
langflow/scripts/ci/sync_bundle_lfx_pin.py
Eric Hare db8935c233 fix(bundles): sync lfx pin to the 1.10.0 line after version realignment (#13516)
The LFX 0.5.0 -> 1.10.0 realignment (#13176) left every src/bundles/*
package and the port_bundle.py generator flooring lfx>=0.5.0. That
silently permits resolving against the now-dead 0.5.x line: a bundle
built against 1.10.0's BUNDLE_API would ImportError there, and the
BUNDLE_API_VERSION check (both "1") would not catch it. RELEASE.md flags
exactly this — the jump "affects downstream pins, and neither pip nor uv
will flag it."

- Bump the 4 bundle pyprojects (arxiv, docling, duckduckgo, ibm) to
  "lfx>=1.10.0,<2.0.0": floored at the current major.minor line, capped
  below the next lfx major. Fine-grained API compat stays enforced via
  extension.json's lfx.compat against BUNDLE_API_VERSION, not the cap.
- Add scripts/ci/sync_bundle_lfx_pin.py and call it from `make patch` so
  future releases keep bundle floors in step. Idempotent: a no-op on
  patch releases, moves the floor on minor/major bumps. Leaves docling
  self-refs and the nightly lfx-nightly== form untouched.
- port_bundle.py now derives the floor from src/lfx/pyproject.toml, so
  newly-ported bundles are born at the current line.
- Update src/bundles/PORTING.md and add test_bundle_lfx_pin.py (20 tests).
2026-06-04 20:23:36 -07:00

122 lines
4.8 KiB
Python

"""Sync the ``lfx`` runtime-dependency floor in every ``src/bundles/*`` package.
After a ``make patch v=X.Y.Z`` version bump, each bundle's ``lfx`` dependency
floor must track Langflow/LFX's ``major.minor`` line: a bundle published from
the X.Y release is built against that lfx's BUNDLE_API surface, so it must be
guaranteed to resolve an lfx new enough to carry it. Before the LFX 0.5.x ->
1.10.0 realignment (#13176) the generated floor was a flat ``lfx>=0.5.0`` with
no upper bound, which silently permitted resolving against the now-dead 0.5.x
line -- and neither pip nor uv flags the cross-line jump.
Pin form: ``lfx>=X.Y.0,<(X+1).0.0`` -- floored at the current minor line,
capped below the next lfx major. The cap is a coarse install-time guard
against an untested lfx major; fine-grained BUNDLE_API compatibility is still
enforced at load time by each ``extension.json``'s ``lfx.compat`` list against
the running lfx's ``BUNDLE_API_VERSION`` (see
``src/lfx/src/lfx/extension/manifest.py``).
Idempotent: re-running with the same version is a no-op (so it is safe to call
unconditionally from ``make patch``, including patch releases within a minor
line where the floor does not move). Only the bundle's ``"lfx<op>..."``
runtime dependency is rewritten -- self-references such as
``"lfx-docling[local]"`` and the nightly ``"lfx-nightly=="`` form are left
untouched (neither has a bare version operator immediately after ``lfx``).
Stdlib only, so it runs in any CI checkout (same constraint as the sibling
``scripts/ci/update_bundle_versions.py`` and ``scripts/migrate/port_bundle.py``).
Usage:
python scripts/ci/sync_bundle_lfx_pin.py 1.10.0
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parents[2]
# Matches a bundle's ``"lfx<op>VERSION[,<UPPER]"`` runtime dependency. The
# version operator immediately after ``lfx`` is what distinguishes the runtime
# dep from self-refs like ``"lfx-docling[local]"`` (a ``-`` follows ``lfx``)
# and the nightly ``"lfx-nightly=="`` rename produced by update_bundle_versions.
_LFX_DEP_PATTERN = re.compile(
r'"lfx(?:>=|~=|==)[\d.]+(?:\.(?:post|dev|a|b|rc)\d+)*'
r'(?:,\s*<[\d.]+(?:\.(?:post|dev|a|b|rc)\d+)*)?"'
)
# Parses the leading ``X.Y`` out of an ``X.Y.Z`` (optionally ``vX.Y.Z``) version.
_VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.\d+")
def lfx_floor_spec(version: str) -> str:
"""Return the bundle ``lfx`` dependency spec for a Langflow/LFX version.
``"1.10.0"`` -> ``"lfx>=1.10.0,<2.0.0"`` (floor at the minor line start,
cap below the next lfx major). A leading ``v`` is tolerated.
NOTE: this floor format is duplicated in ``scripts/migrate/port_bundle.py``
(``_current_lfx_floor``) so each script stays standalone; keep them in step.
"""
match = _VERSION_RE.match(version.lstrip("v"))
if not match:
msg = f"Unparseable version {version!r}; expected X.Y.Z"
raise ValueError(msg)
major, minor = int(match.group(1)), int(match.group(2))
return f"lfx>={major}.{minor}.0,<{major + 1}.0.0"
def rewrite_lfx_dep(content: str, floor_spec: str) -> str:
"""Rewrite the bundle's ``lfx`` runtime dep to ``floor_spec``. Idempotent.
Only the first (runtime) ``"lfx<op>..."`` specifier is touched; self-refs
and the nightly form do not match ``_LFX_DEP_PATTERN``.
"""
return _LFX_DEP_PATTERN.sub(f'"{floor_spec}"', content, count=1)
def sync_bundles(version: str, bundles_dir: Path) -> list[tuple[str, bool]]:
"""Rewrite the ``lfx`` floor in every ``src/bundles/*/pyproject.toml``.
Returns ``(bundle_name, changed)`` tuples, sorted by bundle name.
"""
floor_spec = lfx_floor_spec(version)
results: list[tuple[str, bool]] = []
for pyproject in sorted(bundles_dir.glob("*/pyproject.toml")):
original = pyproject.read_text(encoding="utf-8")
updated = rewrite_lfx_dep(original, floor_spec)
if updated != original:
pyproject.write_text(updated, encoding="utf-8")
results.append((pyproject.parent.name, updated != original))
return results
def main() -> None:
"""Entry point.
Usage:
sync_bundle_lfx_pin.py <version>
``version`` is the Langflow/LFX release version (e.g. ``1.10.0``).
"""
expected_args = 2
if len(sys.argv) != expected_args:
print("Usage: sync_bundle_lfx_pin.py <version>")
sys.exit(1)
version = sys.argv[1]
floor_spec = lfx_floor_spec(version) # validates early
bundles_dir = BASE_DIR / "src" / "bundles"
if not bundles_dir.is_dir():
print("No src/bundles directory; nothing to sync.")
return
print(f'Syncing bundle lfx pin -> "{floor_spec}"')
for name, changed in sync_bundles(version, bundles_dir):
print(f" {'updated' if changed else 'unchanged'}: {name}")
if __name__ == "__main__":
main()