Files
langflow/.github/workflows/release-lfx.yml
Gabriel Luiz Freitas Almeida 1955f8fae5 chore: support Python 3.14 (#13085)
* feat: upgrade Docker images to Python 3.14 (experimental)

## Changes
- Update pyproject.toml: requires-python from <3.14 to <3.15
- Update all 5 Dockerfiles to use Python 3.14:
  - Builder stage: python3.12-trixie-slim → python3.14-trixie-slim
  - Runtime stage: python:3.12-slim-trixie → python:3.14-slim-trixie

## Files Updated
- src/backend/base/pyproject.toml
- docker/build_and_push_base.Dockerfile
- docker/build_and_push.Dockerfile
- docker/build_and_push_backend.Dockerfile
- docker/build_and_push_with_extras.Dockerfile
- docker/build_and_push_ep.Dockerfile

## Rationale
Python 3.14.5 was released on May 10, 2026. Upgrading to the latest
Python version should help reduce CVE vulnerabilities in Docker images.

## Testing Strategy
This is an experimental change to test Python 3.14 compatibility:
- Docker images will use Python 3.14
- CI/CD workflows still test on Python 3.10-3.13
- Nightly build will validate if dependencies work with 3.14
- Can be reverted quickly if issues are found

## Next Steps
- Monitor nightly build for failures
- If successful, update CI/CD workflows to add Python 3.14 to test matrix
- If failures occur, revert and investigate compatibility issues

* chore: support Python 3.14

Bump requires-python upper bound to <3.15 across langflow, langflow-base,
lfx, and langflow-sdk, and add 3.14 to CI test matrices so PRs are gated
on 3.14 compatibility.

Conditional pins for transitive deps without 3.14 wheels at the existing
caps:
- onnxruntime: >=1.26 on 3.14 (existing <1.24 cap retained for 3.10)
- faiss-cpu: >=1.13.2 on 3.14 (existing ==1.9.0.post1 retained for <3.14)

* chore: marker-gate IBM watsonx packages for Python 3.14

ibm-watsonx-ai 1.3.x cannot import on Python 3.14: its StrEnum subclasses
override __init__ in a way that conflicts with 3.14's reworked enum
__set_name__ path (TypeError on KnowledgeBaseFieldRole class creation).

Cap ibm-watsonx-ai, langchain-ibm, and the ibm-watsonx-orchestrate-*
extras at python_version<'3.14' until upstream adapts. Watsonx component
imports are already lazy, so this surfaces only at component-access time
on 3.14, where the existing ImportError handler in
lfx.components.ibm.__init__ degrades it gracefully.

test_model_utils.py imports ChatWatsonx at module top to verify
get_model_name resolves model_id; guard that import so the test
module skips on 3.14 instead of breaking collection.

* [autofix.ci] apply automated fixes

* chore: upgrade remaining Docker images to Python 3.14

build_and_push*.Dockerfile already moved to 3.14 in the merge from
feat/upgrade-python-3.14-docker-images. Apply the same bump to the
dev, devcontainer, and lfx Docker images so all in-repo Dockerfiles
share one Python version.

* [autofix.ci] apply automated fixes

* chore: gate 3.14-broken extras to python_version<'3.14'

cuga (and its transitive fastembed -> py-rust-stemmers source build),
altk/agent-lifecycle-toolkit (re-pulls ibm-watsonx-ai which was already
gated), langchain-pinecone, langwatch, ragstack-ai-knowledge-store, and
OpenDsStar all pin themselves below 3.14 upstream. Without a marker
guard, the workspace lock still tries to install them and a Docker
`uv sync` then attempts source builds that require Rust (CI Docker
test failure with py-rust-stemmers 0.1.5).

Add python_version<'3.14' (or <'3.13' for ragstack) to each pin so
those packages are simply omitted from the 3.14 install set until
upstream catches up. uv pip check is now clean on 3.14.2.

* test: skip test_altk_agent on Python 3.14

altk (agent-lifecycle-toolkit) is gated to python_version<'3.14'
upstream and now via the langflow-base [altk] extra marker. The test
imports altk at module top to exercise ALTKAgentComponent; guard the
import so collection skips on 3.14 instead of failing.

* test: skip remaining altk tests on Python 3.14

Three more test modules import lfx.base.agents.altk_* at module top,
which transitively imports altk. Add the same module-level skip guard
as test_altk_agent.py:
- test_altk_agent_logic.py
- test_altk_agent_tool_conversion.py
- test_conversation_context_ordering.py

* fix(calculator): replace removed ast.Num with ast.Constant for Python 3.14

ast.Num was deprecated in Python 3.8 in favor of ast.Constant and
removed entirely in 3.14. The calculator tool and its core walker
crashed on 3.14 with 'module ast has no attribute Num'.

Switch the isinstance checks to ast.Constant + isinstance(node.value,
(int, float)), and drop the now-dead ast.Num backwards-compat branch
in calculator_core.py (the ast.Constant branch already handles every
input it would have matched). Update the parser unit-test fixtures
to construct ast.Constant nodes.

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* test: skip LangWatch HTTP instrumentation tests on Python 3.14

langwatch is gated to python_version<'3.14' upstream. The
TestLangWatchHttpInstrumentation class patches langwatch.setup and
langwatch.trace inside a fixture, which requires langwatch to be
importable; on 3.14 the test errors with ModuleNotFoundError.

Guard the class with a skipif on langwatch availability.

* test: allow IBM and altk components to be missing on Python 3.14

test_all_modules_importable enforces that every component in __all__
imports cleanly. On 3.14 the ibm-watsonx-ai, langchain-ibm, and altk
packages are gated upstream and intentionally not installed, so the
WatsonxAI, WatsonxEmbeddings, and ALTKAgent components fail to import
as designed.

- test_all_components_in_categories_importable: accept a known-gated
  deny-list on 3.14 instead of failing.
- test_all_lfx_component_modules_directly_importable: extend the
  existing 'missing optional dependency' allowlist with altk,
  langchain_ibm, and ibm_watsonx_ai.

* fix(lfx): pass ensure_exists to user_cache_dir for Python 3.14

Python 3.14 tightened PurePath.__init__ to reject unknown keyword
arguments. The KeyedWorkerLockManager constructor was passing
ensure_exists=True to Path() instead of user_cache_dir(), which
silently worked on 3.10-3.13 but raises TypeError on 3.14 and also
meant the cache directory was never actually being created.

Move the kwarg to user_cache_dir() where it belongs and update the
two unit tests that asserted the buggy call shape.

* [autofix.ci] apply automated fixes

* test: accept either ZIP or JSON error path on Python 3.14

Python 3.14's zipfile.is_zipfile() now validates the central-directory
signature in addition to EOCD, so the garbage+EOCD payload no longer
passes the is_zipfile() dispatch in the /flows/upload/ route. The
endpoint still returns 400 with a descriptive detail (now from the JSON
branch); only the message wording differs, which the test was asserting
verbatim.

* fix(lfx): normalize cors_origins ['*'] back to '*' on Python 3.14

Pydantic-settings on Python 3.14 parses the env var '*' into ['*']
before the cors_origins field validator runs (the list[str] | str union
resolves differently than on 3.10-3.13). Collapse that back to the
bare-string wildcard so downstream consumers — including
warn_about_future_cors_changes and the test suite — see the same shape
on every supported Python version.

---------

Co-authored-by: vijay kumar katuri <vijay.katuri@ibm.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-05-13 22:23:39 +00:00

403 lines
13 KiB
YAML

name: LFX Release
run-name: LFX Release ${{ github.event.inputs.version || 'dev' }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g., 0.1.0)"
required: true
type: string
publish_pypi:
description: "Publish to PyPI"
required: true
type: boolean
default: true
build_docker:
description: "Build and publish Docker images"
required: true
type: boolean
default: true
pre_release:
description: "Mark as pre-release"
required: false
type: boolean
default: false
create_github_release:
description: "Create GitHub release"
required: true
type: boolean
default: true
env:
PYTHON_VERSION: "3.13"
permissions:
contents: write
packages: write
jobs:
validate-version:
name: Validate Version
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check.outputs.should_release }}
current_version: ${{ steps.check.outputs.current_version }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- 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: Check version
id: check
run: |
cd src/lfx
# Use uv tree to get package info, consistent with nightly workflow
name=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $1}')
version=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $2}')
# Strip leading 'v' if present
version=$(echo $version | sed 's/^v//')
echo "current_version=$version" >> $GITHUB_OUTPUT
if [ "$version" != "${{ github.event.inputs.version }}" ]; then
echo "❌ Version mismatch: package has $version but input is ${{ github.event.inputs.version }}"
echo "Please update the version in pyproject.toml first"
echo "should_release=false" >> $GITHUB_OUTPUT
exit 1
fi
# Check if version already exists on PyPI
if curl -s "https://pypi.org/pypi/lfx/json" | jq -r '.releases | keys[]' | grep -q "^${{ github.event.inputs.version }}$"; then
echo "❌ Version ${{ github.event.inputs.version }} already exists on PyPI"
echo "should_release=false" >> $GITHUB_OUTPUT
exit 1
fi
echo "✅ Version ${{ github.event.inputs.version }} is valid and not yet released"
echo "should_release=true" >> $GITHUB_OUTPUT
run-tests:
name: Run Tests
needs: validate-version
if: needs.validate-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Run LFX tests
run: |
cd src/lfx
make test
- name: Test CLI installation
run: |
cd src/lfx
uv pip install .
uv run lfx --help
uv run lfx run --help
uv run lfx serve --help
release-lfx:
name: Build and Release LFX
needs: [validate-version, run-tests]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.check-version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- 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 LFX dependencies
run: uv sync --dev --package lfx
- name: Verify Version
id: check-version
run: |
cd src/lfx
# Use uv tree to get package info, consistent with nightly workflow
name=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $1}')
version=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $2}')
# Verify package name
if [ "$name" != "lfx" ]; then
echo "Package name $name does not match lfx. Exiting the workflow."
exit 1
fi
# Strip leading 'v' if present
version=$(echo $version | sed 's/^v//')
# Verify version matches input
if [ "$version" != "${{ github.event.inputs.version }}" ]; then
echo "Version $version does not match input ${{ github.event.inputs.version }}. Exiting the workflow."
exit 1
fi
echo "version=$version" >> $GITHUB_OUTPUT
- name: Build distribution
run: |
cd src/lfx
rm -rf dist/
uv build --wheel --out-dir dist
- name: Check build artifacts
run: |
cd src/lfx
ls -la dist/
# Verify wheel contents
unzip -l dist/*.whl | grep -E "(lfx/__main__.py|lfx/cli/run.py|lfx/cli/commands.py)"
- name: Test installation from wheel
run: |
cd src/lfx
uv pip install dist/*.whl --force-reinstall
uv run lfx --help
echo "LFX CLI test completed successfully"
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: lfx-dist
path: src/lfx/dist/
retention-days: 5
- name: Publish to PyPI
if: github.event.inputs.publish_pypi == 'true'
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
cd src/lfx
uv publish dist/*.whl
build-docker:
name: Build Docker Images
needs: [validate-version, run-tests]
if: github.event.inputs.build_docker == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
variant: [production, alpine]
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Docker System Info and Cleanup
run: |
echo "=== Docker System Usage Before Cleanup ==="
docker system df || true
docker buildx du || true
echo "=== Cleaning up Docker System ==="
docker system prune -af --volumes || true
docker buildx prune -af || true
echo "=== Docker System Usage After Cleanup ==="
docker system df || true
docker buildx du || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
langflowai/lfx
ghcr.io/langflow-ai/lfx
tags: |
type=raw,value=${{ github.event.inputs.version }}${{ matrix.variant == 'alpine' && '-alpine' || '' }}
type=raw,value=latest${{ matrix.variant == 'alpine' && '-alpine' || '' }},enable=${{ github.event.inputs.pre_release == 'false' }}
labels: |
org.opencontainers.image.title=LFX
org.opencontainers.image.description=Langflow Executor - CLI tool for running Langflow AI workflows
org.opencontainers.image.vendor=Langflow
org.opencontainers.image.version=${{ github.event.inputs.version }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: src/lfx/docker/Dockerfile${{ matrix.variant == 'alpine' && '.alpine' || '' }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
LFX_VERSION=${{ github.event.inputs.version }}
create-release:
name: Create GitHub Release
needs: [release-lfx, build-docker]
if: always() && github.event.inputs.create_github_release == 'true' && needs.release-lfx.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download artifacts
uses: actions/download-artifact@v7
with:
name: lfx-dist
path: dist/
- name: Generate release notes
id: notes
run: |
cat > release_notes.md << EOF
# LFX ${{ github.event.inputs.version }}
## 🚀 Installation
### PyPI
\`\`\`bash
pip install lfx==${{ github.event.inputs.version }}
# or
uv pip install lfx==${{ github.event.inputs.version }}
# or run without installing
uvx lfx@${{ github.event.inputs.version }} --help
\`\`\`
### Docker
\`\`\`bash
# Standard image
docker pull langflowai/lfx:${{ github.event.inputs.version }}
# Alpine image (smaller)
docker pull langflowai/lfx:${{ github.event.inputs.version }}-alpine
# Run a flow
docker run --rm -v \$(pwd):/app/data langflowai/lfx:${{ github.event.inputs.version }} lfx run flow.json --input-value "Hello"
\`\`\`
## 📦 What's New
<!-- Add release notes here -->
## 📋 Checksums
\`\`\`
$(cd dist && sha256sum *)
\`\`\`
---
**Full Changelog**: https://github.com/${{ github.repository }}/compare/v${{ needs.validate-version.outputs.current_version }}...lfx-v${{ github.event.inputs.version }}
EOF
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: lfx-v${{ github.event.inputs.version }}
name: LFX ${{ github.event.inputs.version }}
body_path: release_notes.md
draft: false
prerelease: ${{ github.event.inputs.pre_release }}
files: |
dist/*
generate_release_notes: true
test-release:
name: Test Release
needs: [release-lfx, build-docker]
if: always() && (needs.release-lfx.result == 'success' || needs.build-docker.result == 'success')
runs-on: ubuntu-latest
steps:
- name: Wait for PyPI propagation
if: needs.release-lfx.result == 'success'
run: sleep 60
- name: Test PyPI installation
if: needs.release-lfx.result == 'success'
run: |
# Test installation using uv
uv pip install lfx==${{ github.event.inputs.version }}
uv run lfx --help
- name: Test Docker image
if: needs.build-docker.result == 'success'
run: |
# Test standard image
docker run --rm langflowai/lfx:${{ github.event.inputs.version }} lfx --help
# Test alpine image
docker run --rm langflowai/lfx:${{ github.event.inputs.version }}-alpine lfx --help
# Test with a simple flow
cat > test_flow.json << 'EOF'
{
"nodes": [],
"edges": []
}
EOF
docker run --rm -v $(pwd):/app/data langflowai/lfx:${{ github.event.inputs.version }} \
lfx run /app/data/test_flow.json --input-value "test" || true
notify:
name: Notify Release Status
needs: [create-release, test-release]
if: always()
runs-on: ubuntu-latest
steps:
- name: Notify success
if: needs.create-release.result == 'success'
run: |
echo "✅ LFX ${{ github.event.inputs.version }} released successfully!"
echo "PyPI: https://pypi.org/project/lfx/${{ github.event.inputs.version }}/"
echo "Docker Hub: https://hub.docker.com/r/langflowai/lfx/tags"
echo "GitHub Release: https://github.com/${{ github.repository }}/releases/tag/lfx-v${{ github.event.inputs.version }}"
- name: Notify failure
if: needs.create-release.result != 'success'
run: |
echo "❌ LFX ${{ github.event.inputs.version }} release failed!"
exit 1