diff --git a/.github/workflows/nightly_build.yml b/.github/workflows/nightly_build.yml index e85ec6d40b..c4a39e1175 100644 --- a/.github/workflows/nightly_build.yml +++ b/.github/workflows/nightly_build.yml @@ -163,6 +163,8 @@ jobs: uv run --no-sync ./scripts/ci/update_sdk_version.py $SDK_TAG echo "Updating LFX project version to $LFX_TAG" uv run --no-sync ./scripts/ci/update_lfx_version.py $LFX_TAG $SDK_TAG + echo "Renaming bundle packages to their -nightly counterparts (lfx dev suffix: $LFX_TAG)" + uv run --no-sync ./scripts/ci/update_bundle_versions.py $LFX_TAG echo "Updating base project version to $BASE_TAG and updating main project version to $RELEASE_TAG" uv run --no-sync ./scripts/ci/update_pyproject_combined.py main $RELEASE_TAG $BASE_TAG $LFX_TAG @@ -170,7 +172,7 @@ jobs: cd src/backend/base && uv lock && cd ../../.. cd src/lfx && uv lock && cd ../.. - git add pyproject.toml src/backend/base/pyproject.toml src/lfx/pyproject.toml src/sdk/pyproject.toml uv.lock src/backend/base/uv.lock + git add pyproject.toml src/backend/base/pyproject.toml src/lfx/pyproject.toml src/sdk/pyproject.toml src/bundles/*/pyproject.toml uv.lock src/backend/base/uv.lock git commit -m "Update version and project name" echo "Tagging main with $RELEASE_TAG" diff --git a/.secrets.baseline b/.secrets.baseline index 5f7d690857..648bbb8b98 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -187,7 +187,7 @@ "filename": ".github/workflows/nightly_build.yml", "hashed_secret": "3e26d6750975d678acb8fa35a0f69237881576b0", "is_verified": false, - "line_number": 297, + "line_number": 299, "is_secret": false } ], @@ -9031,5 +9031,5 @@ } ] }, - "generated_at": "2026-05-18T23:09:06Z" + "generated_at": "2026-05-19T01:17:22Z" } diff --git a/scripts/ci/update_bundle_versions.py b/scripts/ci/update_bundle_versions.py new file mode 100644 index 0000000000..40b0726d47 --- /dev/null +++ b/scripts/ci/update_bundle_versions.py @@ -0,0 +1,181 @@ +"""Rename ``lfx-*`` bundle packages to their ``-nightly`` counterparts. + +Bundles under ``src/bundles/*`` follow the same package-rename convention as +``langflow``, ``langflow-base``, ``lfx``, and ``langflow-sdk``: for nightly +builds, the distribution is published as ``-nightly`` so the regular +(non-nightly) PyPI name stays untouched. + +This script (a) renames each bundle's ``[project] name`` to +``-nightly``, (b) bumps its version to ``.dev``, (c) rewrites +its ``lfx`` runtime dep so it resolves against the renamed ``lfx-nightly`` +workspace member, and (d) updates the root ``pyproject.toml`` so its bundle +deps and ``[tool.uv.sources]`` entries reference the renamed packages. + +All operations are idempotent — running twice is a no-op. +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +BASE_DIR = Path(__file__).parent.parent.parent + +# Matches the lfx dep specifier inside a bundle pyproject's dependencies list. +# Accepts the bundle default ("lfx>=X.Y.Z,=[\d.]+(?:\.(?:post|dev|a|b|rc)\d+)*" + r",\s*<[\d.]+(?:\.(?:post|dev|a|b|rc)\d+)*" + r')"' +) + +_PROJECT_NAME_PATTERN = re.compile(r'(\[project\][^\[]*?\nname = ")([^"]+)(")', re.DOTALL) +_PROJECT_VERSION_PATTERN = re.compile(r'(\[project\][^\[]*?\nversion = ")([^"]+)(")', re.DOTALL) + + +def _strip_nightly(name: str) -> str: + """Return the base name with a trailing ``-nightly`` stripped (for idempotency).""" + return name.removesuffix("-nightly") + + +def _strip_dev_suffix(version: str) -> str: + """Return the base version with any trailing PEP 440 dev segment stripped.""" + return re.sub(r"\.dev\d+$", "", version) + + +def _extract_dev_n(tag: str) -> str: + """Extract ``N`` from a tag like ``v0.5.0.dev38`` or ``0.5.0.dev38``.""" + match = re.search(r"\.dev(\d+)$", tag) + if not match: + msg = f"Tag does not end in .devN: {tag!r}" + raise ValueError(msg) + return match.group(1) + + +def rename_bundle_pyproject(pyproject_path: Path, lfx_version: str, dev_n: str) -> tuple[str, str, str] | None: + """Rewrite a single bundle ``pyproject.toml`` for nightly publication. + + - ``[project] name`` → ``-nightly`` + - ``[project] version`` → ``.dev`` + - entry-point key → ``-nightly`` + - ``"lfx>=...,<..."`` dep → ``"lfx-nightly=="`` + + Returns ``(base_name, nightly_name, nightly_version)`` so the caller can + update the root pyproject. Returns ``None`` if the file has no + ``[project]`` name/version (shouldn't happen, but we skip rather than fail). + """ + content = pyproject_path.read_text(encoding="utf-8") + + name_match = _PROJECT_NAME_PATTERN.search(content) + version_match = _PROJECT_VERSION_PATTERN.search(content) + if not name_match or not version_match: + return None + + base_name = _strip_nightly(name_match.group(2)) + nightly_name = f"{base_name}-nightly" + base_version = _strip_dev_suffix(version_match.group(2)) + nightly_version = f"{base_version}.dev{dev_n}" + + content = _PROJECT_NAME_PATTERN.sub(rf"\g<1>{nightly_name}\g<3>", content, count=1) + content = _PROJECT_VERSION_PATTERN.sub(rf"\g<1>{nightly_version}\g<3>", content, count=1) + + # Entry-point key. The key may already be the nightly form on a re-run. + entry_point_pattern = re.compile( + rf'(\[project\.entry-points\."langflow\.extensions"\]\s*\n)' + rf"{re.escape(base_name)}(?:-nightly)?" + rf'(\s*=\s*"[^"]+")' + ) + content = entry_point_pattern.sub(rf"\g<1>{nightly_name}\g<2>", content, count=1) + + # Rewrite the lfx dep regardless of which form it's in. + content = _LFX_DEP_PATTERN.sub(f'"lfx-nightly=={lfx_version}"', content) + + pyproject_path.write_text(content, encoding="utf-8") + return base_name, nightly_name, nightly_version + + +def update_root_pyproject_for_bundle( + root_pyproject: Path, + base_name: str, + nightly_name: str, + nightly_version: str, +) -> None: + """Update root ``pyproject.toml`` to reference the nightly bundle. + + - dependency line ``"[..]"`` → ``"=="`` + - uv.sources entry `` = { workspace = true }`` → `` = ...`` + + Idempotent: also matches the already-nightly form. + """ + content = root_pyproject.read_text(encoding="utf-8") + + # Dependency in [project.dependencies] (any PEP 440 specifier or range form). + dep_pattern = re.compile( + rf'"{re.escape(base_name)}(?:-nightly)?' + r"(?:" + r"(?:~=|==|>=)[\d.]+(?:\.(?:post|dev|a|b|rc)\d+)*" + r"(?:,\s*<[\d.]+(?:\.(?:post|dev|a|b|rc)\d+)*)?" + r')"' + ) + content = dep_pattern.sub(f'"{nightly_name}=={nightly_version}"', content) + + # uv.sources entry — only the workspace = true form is used by bundles today. + source_pattern = re.compile( + rf"^{re.escape(base_name)}(?:-nightly)?(\s*=\s*\{{\s*workspace\s*=\s*true\s*\}})", + re.MULTILINE, + ) + content = source_pattern.sub(rf"{nightly_name}\g<1>", content) + + root_pyproject.write_text(content, encoding="utf-8") + + +def update_bundles_for_nightly(lfx_tag: str) -> list[tuple[str, str, str]]: + """Rename every ``src/bundles/*`` package to its ``-nightly`` counterpart. + + Returns a list of ``(base_name, nightly_name, nightly_version)`` tuples + for the bundles that were rewritten. + """ + bundles_dir = BASE_DIR / "src" / "bundles" + if not bundles_dir.is_dir(): + return [] + + lfx_version = lfx_tag.lstrip("v") + dev_n = _extract_dev_n(lfx_version) + root_pyproject = BASE_DIR / "pyproject.toml" + + results: list[tuple[str, str, str]] = [] + for bundle_pyproject in sorted(bundles_dir.glob("*/pyproject.toml")): + renamed = rename_bundle_pyproject(bundle_pyproject, lfx_version, dev_n) + if renamed is None: + continue + base_name, nightly_name, nightly_version = renamed + update_root_pyproject_for_bundle(root_pyproject, base_name, nightly_name, nightly_version) + results.append(renamed) + print(f"Renamed {base_name} -> {nightly_name} ({nightly_version}) in {bundle_pyproject.relative_to(BASE_DIR)}") + return results + + +def main() -> None: + """Entry point. + + Usage: + update_bundle_versions.py + + ``lfx_tag`` is the LFX nightly tag (e.g., ``v0.5.0.dev38``); its ``.devN`` + suffix is reused for all bundles so they share a single nightly cadence. + """ + expected_args = 2 + if len(sys.argv) != expected_args: + print("Usage: update_bundle_versions.py ") + sys.exit(1) + update_bundles_for_nightly(sys.argv[1]) + + +if __name__ == "__main__": + main()