fix(bundles): floor lfx pin at the minor line's .dev0 so nightlies resolve

The first 1.11.0.dev0 nightly failed its "Test Langflow Main CLI" step:
the fork bump's sync_bundle_lfx_pin.py re-synced every bundle floor to
lfx>=1.11.0, and PEP 440 sorts the nightly's 1.11.0.dev0 BELOW 1.11.0,
so the workspace-built bundles (whose metadata shadows the satisfiable
PyPI lfx-arxiv 0.1.1 during `uv pip install dist/*.whl`) reject the
branch's own lfx while langflow-base pins it exactly — unresolvable.

Floor at lfx>=X.Y.0.dev0,<(X+1).0.0 instead: X.Y.0.dev0 is the lowest
version PEP 440 admits in the minor line, so every devN / rcN / final
satisfies the floor while older lines and the next major stay excluded.
This closes the NIGHTLY.md activation-gate hole structurally — future
minor forks re-sync to a floor their own nightlies already satisfy.

- sync_bundle_lfx_pin.py: lfx_floor_spec emits the .dev0 floor
- port_bundle.py: mirrored _current_lfx_floor kept in step
- bundle pyprojects restamped via the script (arxiv/docling/duckduckgo/ibm)
- test_bundle_lfx_pin.py expectations updated (20/20 passing)
- NIGHTLY.md gate section annotated with the post-activation fix

uv.lock is unaffected (workspace lfx is recorded as an editable source
with no specifier — which is also why uv sync/lock passed in the same
job). The release.yml RC floor-relax sed still matches the new form;
now redundant but harmless. No bundle version bump needed: published
0.1.1 floors >=1.10.0.rc0, satisfiable by the whole 1.11 line.
This commit is contained in:
Eric Hare
2026-06-09 18:02:43 -07:00
parent 7a784515e0
commit ee659ca38c
8 changed files with 51 additions and 27 deletions

View File

@ -8,11 +8,18 @@ guaranteed to resolve an lfx new enough to carry it. Before the LFX 0.5.x ->
no upper bound, which silently permitted resolving against the now-dead 0.5.x 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. 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, Pin form: ``lfx>=X.Y.0.dev0,<(X+1).0.0`` -- floored at the very first
capped below the next lfx major. The cap is a coarse install-time guard pre-release of the current minor line, capped below the next lfx major. The
against an untested lfx major; fine-grained BUNDLE_API compatibility is still ``.dev0`` floor (not ``X.Y.0``) is load-bearing: nightlies off a release
enforced at load time by each ``extension.json``'s ``lfx.compat`` list against branch are canonical ``X.Y.0.devN`` pre-releases, and PEP 440 sorts those
the running lfx's ``BUNDLE_API_VERSION`` (see BELOW ``X.Y.0`` -- a plain ``>=X.Y.0`` floor makes the branch's own nightly
``lfx`` unresolvable against its own bundles (langflow-base pins
``lfx==X.Y.0.devN`` exactly, so the resolver cannot back off). ``X.Y.0.dev0``
is the lowest version PEP 440 admits in the line, so every devN / rcN / final
satisfies it while older minor lines stay excluded. 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``). ``src/lfx/src/lfx/extension/manifest.py``).
Idempotent: re-running with the same version is a no-op (so it is safe to call Idempotent: re-running with the same version is a no-op (so it is safe to call
@ -53,8 +60,10 @@ _VERSION_RE = re.compile(r"^(\d+)\.(\d+)\.\d+")
def lfx_floor_spec(version: str) -> str: def lfx_floor_spec(version: str) -> str:
"""Return the bundle ``lfx`` dependency spec for a Langflow/LFX version. """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, ``"1.11.0"`` -> ``"lfx>=1.11.0.dev0,<2.0.0"`` (floor at the minor line's
cap below the next lfx major). A leading ``v`` is tolerated. first pre-release so the branch's own ``X.Y.0.devN`` nightlies resolve --
see the module docstring; cap below the next lfx major). A leading ``v``
is tolerated.
NOTE: this floor format is duplicated in ``scripts/migrate/port_bundle.py`` NOTE: this floor format is duplicated in ``scripts/migrate/port_bundle.py``
(``_current_lfx_floor``) so each script stays standalone; keep them in step. (``_current_lfx_floor``) so each script stays standalone; keep them in step.
@ -64,7 +73,7 @@ def lfx_floor_spec(version: str) -> str:
msg = f"Unparseable version {version!r}; expected X.Y.Z" msg = f"Unparseable version {version!r}; expected X.Y.Z"
raise ValueError(msg) raise ValueError(msg)
major, minor = int(match.group(1)), int(match.group(2)) major, minor = int(match.group(1)), int(match.group(2))
return f"lfx>={major}.{minor}.0,<{major + 1}.0.0" return f"lfx>={major}.{minor}.0.dev0,<{major + 1}.0.0"
def rewrite_lfx_dep(content: str, floor_spec: str) -> str: def rewrite_lfx_dep(content: str, floor_spec: str) -> str:

View File

@ -69,11 +69,13 @@ def _current_lfx_floor() -> str:
"""Return the ``lfx`` dependency floor for a freshly-ported bundle. """Return the ``lfx`` dependency floor for a freshly-ported bundle.
Reads the workspace lfx version from ``src/lfx/pyproject.toml`` and pins Reads the workspace lfx version from ``src/lfx/pyproject.toml`` and pins
``lfx>=X.Y.0,<(X+1).0.0`` -- floored at the current major.minor line, ``lfx>=X.Y.0.dev0,<(X+1).0.0`` -- floored at the current major.minor
capped below the next lfx major. Mirrors ``lfx_floor_spec`` in line's first pre-release (the branch's own canonical ``X.Y.0.devN``
``scripts/ci/sync_bundle_lfx_pin.py`` (each script is kept standalone, so nightlies sort below a plain ``X.Y.0`` under PEP 440, so they must be
keep the two in step); that script re-syncs every existing bundle on admitted by the floor), capped below the next lfx major. Mirrors
``make patch``. ``lfx_floor_spec`` in ``scripts/ci/sync_bundle_lfx_pin.py`` (each script
is kept standalone, so keep the two in step); that script re-syncs every
existing bundle on ``make patch``.
""" """
lfx_pyproject = (REPO_ROOT / "src" / "lfx" / "pyproject.toml").read_text(encoding="utf-8") lfx_pyproject = (REPO_ROOT / "src" / "lfx" / "pyproject.toml").read_text(encoding="utf-8")
match = re.search(r'^version = "(\d+)\.(\d+)\.\d+', lfx_pyproject, re.MULTILINE) match = re.search(r'^version = "(\d+)\.(\d+)\.\d+', lfx_pyproject, re.MULTILINE)
@ -81,7 +83,7 @@ def _current_lfx_floor() -> str:
msg = "Could not read lfx version from src/lfx/pyproject.toml" msg = "Could not read lfx version from src/lfx/pyproject.toml"
raise ValueError(msg) raise ValueError(msg)
major, minor = int(match.group(1)), int(match.group(2)) major, minor = int(match.group(1)), int(match.group(2))
return f"lfx>={major}.{minor}.0,<{major + 1}.0.0" return f"lfx>={major}.{minor}.0.dev0,<{major + 1}.0.0"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View File

@ -36,15 +36,18 @@ mod = _load_module()
class TestLfxFloorSpec: class TestLfxFloorSpec:
# The .dev0 floor is load-bearing: nightlies are canonical X.Y.0.devN
# pre-releases, which PEP 440 sorts BELOW X.Y.0, so a plain >=X.Y.0
# floor makes the branch's own nightly lfx unresolvable.
@pytest.mark.parametrize( @pytest.mark.parametrize(
("version", "expected"), ("version", "expected"),
[ [
("1.10.0", "lfx>=1.10.0,<2.0.0"), ("1.10.0", "lfx>=1.10.0.dev0,<2.0.0"),
("1.10.3", "lfx>=1.10.0,<2.0.0"), # patch within a minor -> same floor ("1.10.3", "lfx>=1.10.0.dev0,<2.0.0"), # patch within a minor -> same floor
("1.11.0", "lfx>=1.11.0,<2.0.0"), ("1.11.0", "lfx>=1.11.0.dev0,<2.0.0"),
("2.0.0", "lfx>=2.0.0,<3.0.0"), ("2.0.0", "lfx>=2.0.0.dev0,<3.0.0"),
("v1.10.0", "lfx>=1.10.0,<2.0.0"), # leading v tolerated ("v1.10.0", "lfx>=1.10.0.dev0,<2.0.0"), # leading v tolerated
("10.4.2", "lfx>=10.4.0,<11.0.0"), # multi-digit major ("10.4.2", "lfx>=10.4.0.dev0,<11.0.0"), # multi-digit major
], ],
) )
def test_floor(self, version, expected): def test_floor(self, version, expected):
@ -62,7 +65,7 @@ class TestLfxFloorSpec:
class TestRewriteLfxDep: class TestRewriteLfxDep:
FLOOR = "lfx>=1.10.0,<2.0.0" FLOOR = "lfx>=1.10.0.dev0,<2.0.0"
def test_rewrites_bare_floor(self): def test_rewrites_bare_floor(self):
assert mod.rewrite_lfx_dep(' "lfx>=0.5.0",', self.FLOOR) == f' "{self.FLOOR}",' assert mod.rewrite_lfx_dep(' "lfx>=0.5.0",', self.FLOOR) == f' "{self.FLOOR}",'
@ -121,11 +124,11 @@ class TestSyncBundles:
def test_sync_updates_and_reports(self, tmp_path): def test_sync_updates_and_reports(self, tmp_path):
bundles = tmp_path / "bundles" bundles = tmp_path / "bundles"
self._make_bundle(bundles, "arxiv", "lfx>=0.5.0") self._make_bundle(bundles, "arxiv", "lfx>=0.5.0")
self._make_bundle(bundles, "ibm", "lfx>=1.10.0,<2.0.0") # already correct self._make_bundle(bundles, "ibm", "lfx>=1.10.0.dev0,<2.0.0") # already correct
results = dict(mod.sync_bundles("1.10.0", bundles)) results = dict(mod.sync_bundles("1.10.0", bundles))
assert results == {"arxiv": True, "ibm": False} # arxiv changed, ibm no-op assert results == {"arxiv": True, "ibm": False} # arxiv changed, ibm no-op
assert '"lfx>=1.10.0,<2.0.0"' in (bundles / "arxiv" / "pyproject.toml").read_text() assert '"lfx>=1.10.0.dev0,<2.0.0"' in (bundles / "arxiv" / "pyproject.toml").read_text()
def test_sync_idempotent(self, tmp_path): def test_sync_idempotent(self, tmp_path):
bundles = tmp_path / "bundles" bundles = tmp_path / "bundles"

View File

@ -81,6 +81,16 @@ Why (2) is not optional: a bundle pins `lfx>=1.10.0,<2.0.0`, and PEP 440 sorts `
daily from **main's** workflow definition, so merging this before the gate would break the live daily from **main's** workflow definition, so merging this before the gate would break the live
nightly on the next run. nightly on the next run.
> **Post-activation fix (2026-06):** condition (2) as stated was still fragile — the `release-1.11.0`
> fork's `make patch v=1.11.0` re-synced every bundle floor to `lfx>=1.11.0`
> (`scripts/ci/sync_bundle_lfx_pin.py`), which sorts **above** that same branch's `1.11.0.devN`
> nightlies and reintroduced exactly this conflict on the very first `1.11.0.dev0` nightly (the
> workspace-built bundle's metadata shadows the satisfiable PyPI `0.1.1` in the
> `uv pip install dist/*.whl` test step). The synced floor format is now
> `lfx>=X.Y.0.dev0,<(X+1).0.0` — `X.Y.0.dev0` is the lowest version PEP 440 admits in the minor
> line, so every `devN` / `rcN` / final satisfies it while older lines stay excluded, and the gate
> can no longer regress on future minor forks.
## A1 vs A2 + remaining follow-ups (decide before activating) ## A1 vs A2 + remaining follow-ups (decide before activating)
This PR implements the **A1** publish behavior: the separate `langflow-nightly` / `langflow-base-nightly` This PR implements the **A1** publish behavior: the separate `langflow-nightly` / `langflow-base-nightly`

View File

@ -15,7 +15,7 @@ keywords = ["langflow", "lfx", "extension", "bundle", "arxiv", "search"]
# but we list it here as a direct runtime dep so the bundle keeps building # but we list it here as a direct runtime dep so the bundle keeps building
# even if lfx drops it later. # even if lfx drops it later.
dependencies = [ dependencies = [
"lfx>=1.11.0,<2.0.0", "lfx>=1.11.0.dev0,<2.0.0",
"defusedxml>=0.7.1,<1.0.0", "defusedxml>=0.7.1,<1.0.0",
] ]

View File

@ -14,7 +14,7 @@ keywords = ["langflow", "lfx", "extension", "bundle", "docling", "documents"]
# base dependency because the bundle exchanges DoclingDocument objects even when # base dependency because the bundle exchanges DoclingDocument objects even when
# conversion happens remotely. # conversion happens remotely.
dependencies = [ dependencies = [
"lfx>=1.11.0,<2.0.0", "lfx>=1.11.0.dev0,<2.0.0",
"httpx>=0.28.1,<1.0.0", "httpx>=0.28.1,<1.0.0",
"docling-core>=2.36.1,<3.0.0", "docling-core>=2.36.1,<3.0.0",
] ]

View File

@ -18,7 +18,7 @@ keywords = ["langflow", "lfx", "extension", "bundle", "duckduckgo", "search"]
# "lfx": {"compat": [...]} contract. langchain-community uses the same # "lfx": {"compat": [...]} contract. langchain-community uses the same
# range the lfx tree does to keep co-installation lockstep. # range the lfx tree does to keep co-installation lockstep.
dependencies = [ dependencies = [
"lfx>=1.11.0,<2.0.0", "lfx>=1.11.0.dev0,<2.0.0",
"langchain-community>=0.4.1,<1.0.0", "langchain-community>=0.4.1,<1.0.0",
"ddgs>=9.0.0", "ddgs>=9.0.0",
] ]

View File

@ -26,7 +26,7 @@ keywords = ["langflow", "lfx", "extension", "bundle", "ibm", "db2", "watsonx", "
# 3.10-compatible build elsewhere. Kept in lockstep with langflow-base. # 3.10-compatible build elsewhere. Kept in lockstep with langflow-base.
# * ``langchain-community`` provides the helpers DB2VS leans on. # * ``langchain-community`` provides the helpers DB2VS leans on.
dependencies = [ dependencies = [
"lfx>=1.11.0,<2.0.0", "lfx>=1.11.0.dev0,<2.0.0",
"langchain-community>=0.4.1,<1.0.0", "langchain-community>=0.4.1,<1.0.0",
"ibm-db>=3.2.9,<4.0.0; sys_platform != 'linux' or platform_machine != 'aarch64'", "ibm-db>=3.2.9,<4.0.0; sys_platform != 'linux' or platform_machine != 'aarch64'",
"ibm-watsonx-ai>=1.3.1,<2.0.0", "ibm-watsonx-ai>=1.3.1,<2.0.0",