diff --git a/scripts/ci/sync_bundle_lfx_pin.py b/scripts/ci/sync_bundle_lfx_pin.py index 061a0902a4..f79d2a1790 100644 --- a/scripts/ci/sync_bundle_lfx_pin.py +++ b/scripts/ci/sync_bundle_lfx_pin.py @@ -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: diff --git a/scripts/migrate/port_bundle.py b/scripts/migrate/port_bundle.py index c164024dae..86f6c37f01 100755 --- a/scripts/migrate/port_bundle.py +++ b/scripts/migrate/port_bundle.py @@ -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" # --------------------------------------------------------------------------- diff --git a/src/backend/tests/unit/test_bundle_lfx_pin.py b/src/backend/tests/unit/test_bundle_lfx_pin.py index 7225bf1cae..4d9988a250 100644 --- a/src/backend/tests/unit/test_bundle_lfx_pin.py +++ b/src/backend/tests/unit/test_bundle_lfx_pin.py @@ -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" diff --git a/src/bundles/NIGHTLY.md b/src/bundles/NIGHTLY.md index 4344c3852a..75ba6c6b9f 100644 --- a/src/bundles/NIGHTLY.md +++ b/src/bundles/NIGHTLY.md @@ -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` diff --git a/src/bundles/arxiv/pyproject.toml b/src/bundles/arxiv/pyproject.toml index d2c4c29c38..31ce00fa13 100644 --- a/src/bundles/arxiv/pyproject.toml +++ b/src/bundles/arxiv/pyproject.toml @@ -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", ] diff --git a/src/bundles/docling/pyproject.toml b/src/bundles/docling/pyproject.toml index 39a646d3a2..7ab16a0d9c 100644 --- a/src/bundles/docling/pyproject.toml +++ b/src/bundles/docling/pyproject.toml @@ -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", ] diff --git a/src/bundles/duckduckgo/pyproject.toml b/src/bundles/duckduckgo/pyproject.toml index fca53a83eb..65e37f2699 100644 --- a/src/bundles/duckduckgo/pyproject.toml +++ b/src/bundles/duckduckgo/pyproject.toml @@ -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", ] diff --git a/src/bundles/ibm/pyproject.toml b/src/bundles/ibm/pyproject.toml index cf87ba328f..80edf857a1 100644 --- a/src/bundles/ibm/pyproject.toml +++ b/src/bundles/ibm/pyproject.toml @@ -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",