Files
langflow/.github/workflows/release_bundles.yml
Eric Hare 5a8c17b22d ci: backport bundle/nightly CI fixes from main (#13206, #13207, #13208) (#13210)
* ci: stop publishing -nightly bundle variants; release bundles on demand (#13206)

Bundles (lfx-arxiv, lfx-duckduckgo) change infrequently enough that a
nightly cadence is overkill. Drop the rename-to-`-nightly` and bundle
publish lanes from the nightly pipeline and add a dedicated
workflow_dispatch-only release_bundles.yml for purposeful releases.

- nightly_build.yml: drop update_bundle_versions.py invocation and the
  src/bundles/*/pyproject.toml git-add guard.
- release_nightly.yml: remove build-nightly-bundles and
  publish-nightly-bundles jobs; untether test-cross-platform and
  publish-nightly-main from them.
- release_bundles.yml (new): build all src/bundles/* wheels, run a
  cross-OS install smoke test, then publish to PyPI under their stable
  names. Tolerates already-published versions so re-runs without a
  version bump are no-ops.

release.yml still owns bundle publishing as part of main releases.

Manual follow-up (PyPI admin): delete the lfx-arxiv-nightly and
lfx-duckduckgo-nightly projects from PyPI.

* ci(release_bundles): build lfx wheel locally for smoke test (#13207)

The release_bundles.yml smoke test installs bundle wheels into a fresh
venv, which makes uv resolve their `lfx>=X.Y,<Z` pin against PyPI. When
bundles are released ahead of a matching lfx (typical case — bundles
ship more often than lfx bumps land on PyPI), the resolve fails:

  Because only lfx<=0.4.3 is available and lfx-arxiv==0.1.0 depends on
  lfx>=0.5.0,<0.6.0, we can conclude that lfx-arxiv==0.1.0 cannot be
  used.

Build the lfx wheel from the current ref in the build-bundles job and
install it alongside the bundle wheels in the smoke test. The lfx
wheel ships as a separate `dist-lfx-smoketest` artifact and is NOT
included in the publish-bundles step — only the `dist-bundles`
artifact gets pushed to PyPI.

* ci(nightly): re-pin bundle lfx deps to lfx-nightly during nightly build (#13208)

The nightly pipeline renames the workspace `lfx` package to `lfx-nightly`
in `src/lfx/pyproject.toml` and the root workspace dep, but leaves each
`src/bundles/*/pyproject.toml`'s `"lfx>=X.Y,<Z"` pin unchanged. With the
workspace `lfx` gone and PyPI still on lfx 0.4.3, the create-nightly-tag
job's `uv lock` fails:

  × No solution found when resolving dependencies for split [...]
  ╰─▶ Because only lfx<=0.4.3 is available and lfx-arxiv depends on
      lfx>=0.5.0,<0.6.0, we can conclude that lfx-arxiv's requirements
      are unsatisfiable.

`update_lfx_version.py` now also rewrites the `lfx` (or already-rewritten
`lfx-nightly`) specifier in every `src/bundles/*/pyproject.toml` to
`lfx-nightly==<dev version>`, mirroring how `update_lf_base_dependency`
re-pins the lfx dep inside `langflow-base`. The lookahead in the regex
prevents matching sibling packages like `lfx-arxiv` / `lfx-duckduckgo`.

No-op when no bundle pyprojects exist (e.g. on main today), so this is
safe to merge ahead of the bundle extraction landing on main. Restored
the guarded `git add src/bundles/*/pyproject.toml` step so the modified
bundle pyprojects ride the same commit/tag as the rest of the nightly
version bumps.

Cherry-pick to release-1.10.0 to unblock nightlies cut from that branch.
2026-05-19 10:12:54 -07:00

224 lines
7.6 KiB
YAML

name: Release Bundles
# Manually-triggered workflow for publishing stable `lfx-<name>` bundle wheels
# from src/bundles/* to PyPI. Bundles change infrequently, so we release them
# on demand rather than on a schedule. Re-running the workflow without a
# version bump is a no-op (PyPI rejects the duplicate upload and we tolerate
# the conflict).
on:
workflow_dispatch:
inputs:
release_tag:
description: "Git ref to build from (branch, tag, or SHA). Defaults to the workflow's checked-out ref."
required: false
type: string
default: ""
dry_run:
description: "Build and test only; skip the PyPI publish step."
required: false
type: boolean
default: false
env:
PYTHON_VERSION: "3.13"
jobs:
build-bundles:
name: Build Langflow Bundles
runs-on: ubuntu-latest
defaults:
run:
shell: bash
outputs:
bundles-found: ${{ steps.build.outputs.bundles-found }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag || github.ref }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ env.PYTHON_VERSION }}
prune-cache: false
- name: Install the project
run: uv sync
- name: Build bundle wheels
id: build
run: |
shopt -s nullglob
bundles=(src/bundles/*/pyproject.toml)
if [ ${#bundles[@]} -eq 0 ]; then
echo "No bundles found under src/bundles/*/"
echo "bundles-found=0" >> $GITHUB_OUTPUT
exit 0
fi
mkdir -p bundles-dist
BUNDLES_DIST="$PWD/bundles-dist"
for bundle_pyproject in "${bundles[@]}"; do
bundle_dir=$(dirname "$bundle_pyproject")
echo "Building wheel for $bundle_dir"
(cd "$bundle_dir" && uv build --wheel --out-dir "$BUNDLES_DIST")
done
echo "bundles-found=1" >> $GITHUB_OUTPUT
ls -la bundles-dist/
# Bundles pin lfx (e.g. lfx>=0.5.0,<0.6.0). The smoke test below
# installs the bundles in a fresh venv, so it needs an lfx wheel that
# satisfies the pin available locally — PyPI may not yet have a
# matching lfx published when bundles are released ahead of a main
# release. We build the lfx wheel from the current ref so the smoke
# test resolves against it; this artifact is not published.
- name: Build lfx wheel for smoke test
if: steps.build.outputs.bundles-found == '1'
run: |
mkdir -p lfx-dist
LFX_DIST="$PWD/lfx-dist"
(cd src/lfx && uv build --wheel --out-dir "$LFX_DIST")
ls -la lfx-dist/
- name: Upload bundles artifact
if: steps.build.outputs.bundles-found == '1'
uses: actions/upload-artifact@v6
with:
name: dist-bundles
path: bundles-dist
- name: Upload lfx artifact (smoke-test only; not published)
if: steps.build.outputs.bundles-found == '1'
uses: actions/upload-artifact@v6
with:
name: dist-lfx-smoketest
path: lfx-dist
test-install:
name: Install Smoke Test - ${{ matrix.os }} ${{ matrix.python-version }}
needs: [build-bundles]
if: needs.build-bundles.outputs.bundles-found == '1'
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- os: linux
runner: ubuntu-latest
python-version: "3.10"
- os: linux
runner: ubuntu-latest
python-version: "3.12"
- os: macos
runner: macos-latest
python-version: "3.12"
- os: windows
runner: windows-latest
python-version: "3.12"
steps:
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Setup UV
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
ignore-empty-workdir: true
- name: Download bundles artifact
uses: actions/download-artifact@v7
with:
name: dist-bundles
path: ./bundles-dist
- name: Download lfx artifact
uses: actions/download-artifact@v7
with:
name: dist-lfx-smoketest
path: ./lfx-dist
- name: Create fresh virtual environment
run: uv venv test-env --seed
shell: bash
- name: Install lfx + bundle wheels (Unix)
if: matrix.os != 'windows'
run: |
shopt -s nullglob
BUNDLE_WHEELS=(./bundles-dist/*.whl)
LFX_WHEELS=(./lfx-dist/*.whl)
if [ ${#BUNDLE_WHEELS[@]} -eq 0 ]; then
echo "No bundle wheels found in ./bundles-dist/"
exit 1
fi
if [ ${#LFX_WHEELS[@]} -eq 0 ]; then
echo "No lfx wheel found in ./lfx-dist/"
exit 1
fi
printf 'Installing: %s\n' "${LFX_WHEELS[@]}" "${BUNDLE_WHEELS[@]}"
uv pip install --prerelease=allow --python ./test-env/bin/python "${LFX_WHEELS[@]}" "${BUNDLE_WHEELS[@]}"
shell: bash
- name: Install lfx + bundle wheels (Windows)
if: matrix.os == 'windows'
run: |
shopt -s nullglob
BUNDLE_WHEELS=(./bundles-dist/*.whl)
LFX_WHEELS=(./lfx-dist/*.whl)
if [ ${#BUNDLE_WHEELS[@]} -eq 0 ]; then
echo "No bundle wheels found in ./bundles-dist/"
exit 1
fi
if [ ${#LFX_WHEELS[@]} -eq 0 ]; then
echo "No lfx wheel found in ./lfx-dist/"
exit 1
fi
printf 'Installing: %s\n' "${LFX_WHEELS[@]}" "${BUNDLE_WHEELS[@]}"
uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "${LFX_WHEELS[@]}" "${BUNDLE_WHEELS[@]}"
shell: bash
publish-bundles:
name: Publish Bundles to PyPI
needs: [build-bundles, test-install]
if: |
always() &&
!cancelled() &&
!inputs.dry_run &&
needs.build-bundles.result == 'success' &&
needs.build-bundles.outputs.bundles-found == '1' &&
needs.test-install.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Download bundles artifact
uses: actions/download-artifact@v7
with:
name: dist-bundles
path: bundles-dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: ${{ env.PYTHON_VERSION }}
- name: Publish bundles to PyPI
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
shopt -s nullglob
wheels=(bundles-dist/*.whl)
if [ ${#wheels[@]} -eq 0 ]; then
echo "No bundle wheels to publish."
exit 0
fi
failed=0
for wheel in "${wheels[@]}"; do
echo "Publishing $wheel"
# Capture stderr so we can tolerate "already exists" without failing
# the whole run — re-running this workflow without bumping a bundle
# version should be a no-op for that bundle.
if ! err=$(uv publish "$wheel" 2>&1); then
echo "$err"
if echo "$err" | grep -qiE 'already exists|file already exists|HTTP 400|duplicate'; then
echo "Skipping $wheel: already published."
else
echo "Publish failed for $wheel"
failed=1
fi
fi
done
if [ "$failed" = "1" ]; then
exit 1
fi