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
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
Pin form: ``lfx>=X.Y.0.dev0,<(X+1).0.0`` -- floored at the very first
pre-release of the current minor line, capped below the next lfx major. The
``.dev0`` floor (not ``X.Y.0``) is load-bearing: nightlies off a release
branch are canonical ``X.Y.0.devN`` pre-releases, and PEP 440 sorts those
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``).
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:
"""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.
``"1.11.0"`` -> ``"lfx>=1.11.0.dev0,<2.0.0"`` (floor at the minor line's
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``
(``_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"
raise ValueError(msg)
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:

View File

@ -69,11 +69,13 @@ def _current_lfx_floor() -> str:
"""Return the ``lfx`` dependency floor for a freshly-ported bundle.
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,
capped below the next lfx major. Mirrors ``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>=X.Y.0.dev0,<(X+1).0.0`` -- floored at the current major.minor
line's first pre-release (the branch's own canonical ``X.Y.0.devN``
nightlies sort below a plain ``X.Y.0`` under PEP 440, so they must be
admitted by the floor), capped below the next lfx major. Mirrors
``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")
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"
raise ValueError(msg)
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:
# 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(
("version", "expected"),
[
("1.10.0", "lfx>=1.10.0,<2.0.0"),
("1.10.3", "lfx>=1.10.0,<2.0.0"), # patch within a minor -> same floor
("1.11.0", "lfx>=1.11.0,<2.0.0"),
("2.0.0", "lfx>=2.0.0,<3.0.0"),
("v1.10.0", "lfx>=1.10.0,<2.0.0"), # leading v tolerated
("10.4.2", "lfx>=10.4.0,<11.0.0"), # multi-digit major
("1.10.0", "lfx>=1.10.0.dev0,<2.0.0"),
("1.10.3", "lfx>=1.10.0.dev0,<2.0.0"), # patch within a minor -> same floor
("1.11.0", "lfx>=1.11.0.dev0,<2.0.0"),
("2.0.0", "lfx>=2.0.0.dev0,<3.0.0"),
("v1.10.0", "lfx>=1.10.0.dev0,<2.0.0"), # leading v tolerated
("10.4.2", "lfx>=10.4.0.dev0,<11.0.0"), # multi-digit major
],
)
def test_floor(self, version, expected):
@ -62,7 +65,7 @@ class TestLfxFloorSpec:
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):
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):
bundles = tmp_path / "bundles"
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))
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):
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
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)
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
# even if lfx drops it later.
dependencies = [
"lfx>=1.11.0,<2.0.0",
"lfx>=1.11.0.dev0,<2.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
# conversion happens remotely.
dependencies = [
"lfx>=1.11.0,<2.0.0",
"lfx>=1.11.0.dev0,<2.0.0",
"httpx>=0.28.1,<1.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
# range the lfx tree does to keep co-installation lockstep.
dependencies = [
"lfx>=1.11.0,<2.0.0",
"lfx>=1.11.0.dev0,<2.0.0",
"langchain-community>=0.4.1,<1.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.
# * ``langchain-community`` provides the helpers DB2VS leans on.
dependencies = [
"lfx>=1.11.0,<2.0.0",
"lfx>=1.11.0.dev0,<2.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-watsonx-ai>=1.3.1,<2.0.0",