Files
langflow/.github/workflows/release.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

1076 lines
40 KiB
YAML

name: Langflow Release
run-name: Langflow Release by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
release_tag:
description: "Tag to release from. This is the tag that contains the source code for the release."
required: true
type: string
release_package_base:
description: "Release Langflow Base"
required: true
type: boolean
default: false
release_package_main:
description: "Release Langflow"
required: true
type: boolean
default: false
release_lfx:
description: "Release LFX package (manually triggered)"
required: false
type: boolean
default: false
release_sdk:
description: "Release langflow-sdk package (required when releasing LFX)"
required: false
type: boolean
default: false
build_docker_base:
description: "Build Docker Image for Langflow Base"
required: true
type: boolean
default: false
build_docker_main:
description: "Build Docker Image for Langflow"
required: true
type: boolean
default: false
pre_release:
description: "Pre-release"
required: false
type: boolean
default: false
create_release:
description: "Whether to create a gh release"
required: false
type: boolean
default: false
dry_run:
description: "Dry run mode - disables all pushes to external services (PyPI, Docker, GitHub releases)"
required: false
type: boolean
default: true
run_pip_check:
description: "Whether to run pip check - ONLY SET TO FALSE ONCE YOU HAVE ALREADY ATTEMPTED AND FAILED A RUN THEN MADE SURE DEPS DO NOT CAUSE FAILURES"
required: false
type: boolean
default: true
jobs:
echo-inputs:
name: Echo Workflow Inputs
runs-on: ubuntu-latest
steps:
- name: Echo workflow inputs
run: |
echo "release_tag: ${{ inputs.release_tag }}"
echo "release_package_base: ${{ inputs.release_package_base }}"
echo "release_package_main: ${{ inputs.release_package_main }}"
echo "release_lfx: ${{ inputs.release_lfx }}"
echo "release_sdk: ${{ inputs.release_sdk }}"
echo "build_docker_base: ${{ inputs.build_docker_base }}"
echo "build_docker_main: ${{ inputs.build_docker_main }}"
echo "pre_release: ${{ inputs.pre_release }}"
echo "create_release: ${{ inputs.create_release }}"
echo "dry_run: ${{ inputs.dry_run }}"
validate-tag:
name: Validate Tag Input
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0 # Fetch all history - required for tags (?)
- name: Validate that input is a tag, not a branch
run: |
# Check if the input exists as a tag
if ! git tag -l | grep -q "^${{ inputs.release_tag }}$"; then
echo "Error: '${{ inputs.release_tag }}' is not a valid tag."
echo "Available tags:"
git tag -l | head -20
exit 1
fi
# Check if the input also exists as a branch (warn if so, but don't fail)
if git branch -r | grep -q "origin/${{ inputs.release_tag }}$"; then
echo "Tag '${{ inputs.release_tag }}' also exists as a branch. Exiting out of caution."
exit 1
fi
echo "Validated: '${{ inputs.release_tag }}' is a valid tag."
validate-tag-format:
name: Validate Tag Format and Check for Duplicates
runs-on: ubuntu-latest
needs: [validate-tag]
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate Tag Has v Prefix
run: |
TAG="${{ inputs.release_tag }}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "❌ Error: Tag must start with 'v' and follow semver format (v1.2.3)"
echo " Got: $TAG"
echo ""
echo "This is required to ensure GitHub's generateReleaseNotes uses the correct base comparison."
exit 1
fi
echo "✅ Tag format is valid: $TAG"
- name: Check for Duplicate Tag Without v Prefix
run: |
TAG="${{ inputs.release_tag }}"
TAG_WITHOUT_V="${TAG#v}"
if git rev-parse "$TAG_WITHOUT_V" >/dev/null 2>&1; then
echo "❌ Error: Duplicate tag without 'v' prefix exists: $TAG_WITHOUT_V"
echo " This will cause release notes to use the wrong base comparison."
echo ""
echo " The tag '$TAG_WITHOUT_V' points to commit: $(git rev-parse --short $TAG_WITHOUT_V)"
echo " The tag '$TAG' points to commit: $(git rev-parse --short $TAG)"
echo ""
echo " To fix this, delete the duplicate tag:"
echo " git push origin :refs/tags/$TAG_WITHOUT_V"
exit 1
fi
echo "✅ No duplicate tag found"
validate-dependencies:
name: Validate Release Dependencies
runs-on: ubuntu-latest
if: ${{ inputs.release_package_base || inputs.release_package_main || inputs.release_lfx || inputs.release_sdk || inputs.build_docker_base || inputs.build_docker_main }}
needs: [validate-tag]
steps:
- name: Validate that build-base is enabled if build-main is enabled
run: |
if [ "${{ inputs.release_package_main }}" = "true" ] && [ "${{ inputs.release_package_base }}" = "false" ]; then
echo "Error: Cannot release Langflow Main without releasing Langflow Base."
echo "Please enable 'release_package_base' or disable 'release_package_main'."
exit 1
fi
if [ "${{ inputs.release_lfx }}" = "true" ] && [ "${{ inputs.release_sdk }}" = "false" ]; then
echo "Error: Cannot release LFX without releasing langflow-sdk."
echo "LFX depends on langflow-sdk. Please enable 'release_sdk' or disable 'release_lfx'."
exit 1
fi
echo "✅ Release dependencies validated successfully."
determine-base-version:
name: Determine Base Version
needs: [validate-tag, validate-dependencies]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Install the project
run: uv sync
- name: Determine version
id: version
run: |
version=$(python3 -c "
import tomllib, pathlib
data = tomllib.loads(pathlib.Path('src/backend/base/pyproject.toml').read_text())
print(data['project']['version'])
")
echo "Base version from pyproject.toml: $version"
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://pypi.org/pypi/langflow-base/json" | jq -r '.releases | keys | .[]' | grep -E '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")"
echo "Latest base pre-release version: $last_released_version"
echo "Base pre-release version to be released: $version"
else
last_released_version=$(curl -s "https://pypi.org/pypi/langflow-base/json" | jq -r '.releases | keys | .[]' | grep -vE '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
echo "Latest base release version: $last_released_version"
fi
if [ "$version" = "$last_released_version" ]; then
echo "Base pypi version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 1
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
determine-main-version:
name: Determine Main Version
needs: [validate-tag, validate-dependencies]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Install the project
run: uv sync
- name: Determine version
id: version
run: |
version=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | grep -v 'langflow-sdk' | awk '{print $2}' | sed 's/^v//')
echo "Main version from pyproject.toml: $version"
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://pypi.org/pypi/langflow/json" | jq -r '.releases | keys | .[]' | grep -E '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")"
echo "Latest main pre-release version: $last_released_version"
echo "Main pre-release version to be released: $version"
else
last_released_version=$(curl -s "https://pypi.org/pypi/langflow/json" | jq -r '.releases | keys | .[]' | grep -vE '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
echo "Latest main release version: $last_released_version"
fi
if [ "$version" = "$last_released_version" ]; then
echo "Main pypi version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 1
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
determine-lfx-version:
name: Determine LFX Version
needs: [validate-tag, validate-dependencies]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Install LFX dependencies
run: uv sync --dev --package lfx
- name: Determine version
id: version
run: |
version=$(uv tree | grep 'lfx' | awk '{print $3}' | sed 's/^v//' | head -n 2 | xargs)
echo "LFX version from pyproject.toml: $version"
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://pypi.org/pypi/lfx/json" | jq -r '.releases | keys | .[]' | grep -E '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")"
echo "Latest LFX pre-release version: $last_released_version"
echo "LFX pre-release version to be released: $version"
else
last_released_version=$(curl -s "https://pypi.org/pypi/lfx/json" | jq -r '.releases | keys | .[]' | grep -vE '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
echo "Latest LFX release version: $last_released_version"
fi
if [ "$version" = "$last_released_version" ]; then
echo "LFX pypi version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 1
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
determine-sdk-version:
name: Determine SDK Version
needs: [validate-tag, validate-dependencies]
if: ${{ inputs.release_sdk || inputs.release_lfx }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Install SDK dependencies
run: uv sync --dev --package langflow-sdk
- name: Determine version
id: version
run: |
version=$(python3 -c "
import tomllib, pathlib
data = tomllib.loads(pathlib.Path('src/sdk/pyproject.toml').read_text())
print(data['project']['version'])
")
echo "SDK version from pyproject.toml: $version"
# Check if langflow-sdk exists on PyPI (may be first release)
pypi_response=$(curl -s -o /dev/null -w "%{http_code}" "https://pypi.org/pypi/langflow-sdk/json")
if [ "$pypi_response" = "404" ]; then
echo "langflow-sdk has never been published to PyPI. This will be the first release."
last_released_version=""
else
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://pypi.org/pypi/langflow-sdk/json" | jq -r '.releases | keys | .[]' | grep -E '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")"
echo "Latest SDK pre-release version: $last_released_version"
echo "SDK pre-release version to be released: $version"
else
last_released_version=$(curl -s "https://pypi.org/pypi/langflow-sdk/json" | jq -r '.releases | keys | .[]' | grep -vE '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
echo "Latest SDK release version: $last_released_version"
fi
fi
if [ "$version" = "$last_released_version" ]; then
echo "SDK pypi version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 1
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
ci:
name: CI
needs: [validate-tag, validate-dependencies]
uses: ./.github/workflows/ci.yml
with:
ref: ${{ inputs.release_tag }}
python-versions: "['3.10', '3.11', '3.12', '3.13', '3.14']"
frontend-tests-folder: "tests"
release: true
run-all-tests: true
runs-on: ubuntu-latest
secrets: inherit
build-sdk:
name: Build langflow-sdk
needs: [determine-sdk-version]
if: ${{ needs.determine-sdk-version.outputs.skipped == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Set version for pre-release
if: ${{ inputs.pre_release }}
run: |
VERSION="${{ needs.determine-sdk-version.outputs.version }}"
echo "Setting SDK pre-release version to: $VERSION"
cd src/sdk
# Update version in SDK pyproject.toml
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
# Verify the change
echo "Updated pyproject.toml version:"
grep "^version" pyproject.toml
- name: Build project for distribution
run: make sdk_build args="--wheel"
- name: Verify built version
run: |
EXPECTED_VERSION="${{ needs.determine-sdk-version.outputs.version }}"
WHEEL_FILE=$(ls src/sdk/dist/*.whl)
echo "Built wheel: $WHEEL_FILE"
NORMALIZED_VERSION=$(echo "$EXPECTED_VERSION" | sed 's/\.rc/rc/g; s/\.a/a/g; s/\.b/b/g; s/\.dev/dev/g')
echo "Expected version: $EXPECTED_VERSION"
echo "Normalized for wheel: $NORMALIZED_VERSION"
if [[ ! "$WHEEL_FILE" =~ $NORMALIZED_VERSION ]]; then
echo "❌ Error: Wheel version doesn't match expected version"
echo "Expected: $EXPECTED_VERSION (normalized: $NORMALIZED_VERSION)"
echo "Wheel file: $WHEEL_FILE"
exit 1
fi
echo "✅ Version verified: $EXPECTED_VERSION"
- name: Test SDK import
run: |
cd src/sdk
uv venv test-env --seed
uv pip install --python ./test-env/bin/python dist/*.whl
EXPECTED_VERSION="${{ needs.determine-sdk-version.outputs.version }}"
./test-env/bin/python -c "from importlib.metadata import version; import langflow_sdk; installed_version = version('langflow-sdk'); print(f'langflow-sdk {installed_version} imported successfully'); assert installed_version == '$EXPECTED_VERSION', f'Installed version {installed_version} does not match expected version $EXPECTED_VERSION'"
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: dist-sdk
path: src/sdk/dist
build-lfx:
name: Build LFX
needs: [determine-lfx-version, determine-sdk-version, build-sdk]
if: |
always() &&
!cancelled() &&
needs.determine-lfx-version.result == 'success' &&
needs.determine-lfx-version.outputs.skipped == 'false'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Download SDK artifact
if: needs.build-sdk.result == 'success'
uses: actions/download-artifact@v7
with:
name: dist-sdk
path: ./sdk-dist
- name: Install LFX dependencies
run: uv sync --dev --package lfx
- name: Set version for pre-release
if: ${{ inputs.pre_release }}
run: |
VERSION="${{ needs.determine-lfx-version.outputs.version }}"
echo "Setting pre-release version to: $VERSION"
cd src/lfx
# Update version in lfx pyproject.toml
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
# Verify the change
echo "Updated pyproject.toml version:"
grep "^version" pyproject.toml
- name: Update langflow-sdk dependency for pre-release
if: ${{ inputs.pre_release && needs.build-sdk.result == 'success' }}
run: |
SDK_VERSION="${{ needs.determine-sdk-version.outputs.version }}"
echo "Updating langflow-sdk dependency to allow pre-release version: $SDK_VERSION"
cd src/lfx
# Extract current langflow-sdk constraint from pyproject.toml
CURRENT_CONSTRAINT=$(grep -E '^\s*"langflow-sdk' pyproject.toml | head -1)
echo "Current constraint: $CURRENT_CONSTRAINT"
# Create new constraint using the next major derived from SDK_VERSION
SDK_BASE_VERSION=$(echo "$SDK_VERSION" | sed 's/[^0-9.].*$//')
SDK_MAJOR=$(echo "$SDK_BASE_VERSION" | cut -d. -f1)
NEXT_MAJOR=$((SDK_MAJOR + 1))
NEW_CONSTRAINT="\"langflow-sdk>=$SDK_VERSION,<$NEXT_MAJOR.dev0\""
echo "New constraint: $NEW_CONSTRAINT"
# Replace the constraint
sed -i.bak "s|\"langflow-sdk[^\"]*\"|$NEW_CONSTRAINT|" pyproject.toml
# Verify the change
echo "Updated langflow-sdk dependency:"
grep "langflow-sdk" pyproject.toml
- name: Build project for distribution
run: |
cd src/lfx
rm -rf dist/
uv build --wheel --out-dir dist
- name: Verify built version
run: |
EXPECTED_VERSION="${{ needs.determine-lfx-version.outputs.version }}"
WHEEL_FILE=$(ls src/lfx/dist/*.whl)
echo "Built wheel: $WHEEL_FILE"
NORMALIZED_VERSION=$(echo "$EXPECTED_VERSION" | sed 's/\.rc/rc/g; s/\.a/a/g; s/\.b/b/g; s/\.dev/dev/g')
echo "Expected version: $EXPECTED_VERSION"
echo "Normalized for wheel: $NORMALIZED_VERSION"
if [[ ! "$WHEEL_FILE" =~ $NORMALIZED_VERSION ]]; then
echo "❌ Error: Wheel version doesn't match expected version"
echo "Expected: $EXPECTED_VERSION (normalized: $NORMALIZED_VERSION)"
echo "Wheel file: $WHEEL_FILE"
exit 1
fi
echo "✅ Version verified: $EXPECTED_VERSION"
- name: Test CLI
run: |
cd src/lfx
# Use a clean venv and install both SDK and LFX wheels together
# so uv can resolve the local SDK dependency without falling back to PyPI.
uv venv test-env --seed
if [ -d "../../sdk-dist" ]; then
SDK_WHEEL=$(find ../../sdk-dist -name "*.whl" -type f | head -1)
uv pip install --python ./test-env/bin/python --force-reinstall "$SDK_WHEEL" dist/*.whl
else
uv pip install --python ./test-env/bin/python --force-reinstall dist/*.whl
fi
./test-env/bin/lfx --help
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: dist-lfx
path: src/lfx/dist
build-base:
name: Build Langflow Base
needs: [build-lfx, build-sdk, determine-base-version, determine-lfx-version]
if: |
always() &&
!cancelled() &&
inputs.release_package_base &&
needs.determine-base-version.result == 'success' &&
needs.determine-base-version.outputs.skipped == 'false' &&
needs.build-lfx.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.12"
prune-cache: false
- name: Download LFX artifact
uses: actions/download-artifact@v7
with:
name: dist-lfx
path: ./lfx-dist
- name: Download SDK artifact
if: needs.build-sdk.result == 'success'
uses: actions/download-artifact@v7
with:
name: dist-sdk
path: ./sdk-dist
- name: Install dependencies with local LFX and SDK wheels
run: |
# Create virtual environment
uv venv
# Install using pip with local wheel directories as find-links
FIND_LINKS="--find-links ./lfx-dist"
if [ -d "./sdk-dist" ]; then
FIND_LINKS="$FIND_LINKS --find-links ./sdk-dist"
fi
uv pip install $FIND_LINKS --prerelease=allow -e src/backend/base
- name: Check for dependency incompatibility
if: ${{ inputs.run_pip_check }}
run: uv pip check
- name: Set version for pre-release
if: ${{ inputs.pre_release }}
run: |
VERSION="${{ needs.determine-base-version.outputs.version }}"
echo "Setting pre-release version to: $VERSION"
cd src/backend/base
# Update version in pyproject.toml
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
# Verify the change
echo "Updated pyproject.toml version:"
grep "^version" pyproject.toml
- name: Update lfx dependency for pre-release
if: ${{ inputs.pre_release }}
run: |
LFX_VERSION="${{ needs.determine-lfx-version.outputs.version }}"
echo "Updating lfx dependency to allow pre-release version: $LFX_VERSION"
cd src/backend/base
# Extract current lfx constraint from pyproject.toml
CURRENT_CONSTRAINT=$(grep -E '^\s*"lfx' pyproject.toml | head -1)
echo "Current constraint: $CURRENT_CONSTRAINT"
# Extract the major.minor version (e.g., "0.3" from "~=0.3.0")
MAJOR_MINOR=$(echo "$CURRENT_CONSTRAINT" | sed -E 's/.*[~>=<]+([0-9]+\.[0-9]+).*/\1/')
NEXT_MAJOR=$((${MAJOR_MINOR%.*} + 1))
# Create new constraint: >=LFX_VERSION,<NEXT_MAJOR.dev0
NEW_CONSTRAINT="\"lfx>=$LFX_VERSION,<$NEXT_MAJOR.dev0\""
echo "New constraint: $NEW_CONSTRAINT"
# Replace the constraint
sed -i.bak "s|\"lfx[^\"]*\"|$NEW_CONSTRAINT|" pyproject.toml
# Verify the change
echo "Updated lfx dependency:"
grep "lfx" pyproject.toml
- name: Build project for distribution
run: make build base=true args="--wheel"
- name: Verify built version
run: |
EXPECTED_VERSION="${{ needs.determine-base-version.outputs.version }}"
WHEEL_FILE=$(ls dist/*.whl 2>/dev/null || ls src/backend/base/dist/*.whl)
echo "Built wheel: $WHEEL_FILE"
NORMALIZED_VERSION=$(echo "$EXPECTED_VERSION" | sed 's/\.rc/rc/g; s/\.a/a/g; s/\.b/b/g; s/\.dev/dev/g')
echo "Expected version: $EXPECTED_VERSION"
echo "Normalized for wheel: $NORMALIZED_VERSION"
if [[ ! "$WHEEL_FILE" =~ $NORMALIZED_VERSION ]]; then
echo "❌ Error: Wheel version doesn't match expected version"
echo "Expected: $EXPECTED_VERSION (normalized: $NORMALIZED_VERSION)"
echo "Wheel file: $WHEEL_FILE"
exit 1
fi
echo "✅ Version verified: $EXPECTED_VERSION"
- name: Test CLI
run: |
# TODO: Unsure why the whl is not built in src/backend/base/dist
mkdir src/backend/base/dist
mv dist/*.whl src/backend/base/dist
uv pip install src/backend/base/dist/*.whl
uv run python -m langflow run --host localhost --port 7860 --backend-only &
SERVER_PID=$!
# Wait for the server to start
timeout 120 bash -c 'until curl -f http://localhost:7860/api/v1/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1)
# Terminate the server
kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1)
sleep 20 # give the server some time to terminate
# Check if the server is still running
if kill -0 $SERVER_PID 2>/dev/null; then
echo "Failed to terminate the server"
exit 0
else
echo "Server terminated successfully"
fi
# PyPI publishing moved to after cross-platform testing
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: dist-base
path: src/backend/base/dist
build-main:
name: Build Langflow Main
needs:
[
build-base,
build-lfx,
build-sdk,
determine-base-version,
determine-main-version,
determine-lfx-version,
]
if: |
always() &&
!cancelled() &&
inputs.release_package_main &&
needs.determine-main-version.result == 'success' &&
needs.determine-main-version.outputs.skipped == 'false' &&
needs.build-base.result == 'success' &&
needs.build-lfx.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.12"
prune-cache: false
- name: Download LFX artifact
uses: actions/download-artifact@v7
with:
name: dist-lfx
path: ./lfx-dist
- name: Download SDK artifact
if: needs.build-sdk.result == 'success'
uses: actions/download-artifact@v7
with:
name: dist-sdk
path: ./sdk-dist
- name: Download base artifact
uses: actions/download-artifact@v7
with:
name: dist-base
path: ./base-dist
- name: Install dependencies with local wheels
run: |
# Create virtual environment
uv venv
# Install using pip with local wheel directories as find-links
FIND_LINKS="--find-links ./lfx-dist --find-links ./base-dist"
if [ -d "./sdk-dist" ]; then
FIND_LINKS="$FIND_LINKS --find-links ./sdk-dist"
fi
uv pip install $FIND_LINKS --prerelease=allow -e .
- name: Check for dependency incompatibility
if: ${{ inputs.run_pip_check }}
run: uv pip check
- name: Set version for pre-release
if: ${{ inputs.pre_release }}
run: |
VERSION="${{ needs.determine-main-version.outputs.version }}"
echo "Setting main pre-release version to: $VERSION"
# Update version in main pyproject.toml
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
# Verify the change
echo "Updated pyproject.toml version:"
grep "^version" pyproject.toml
- name: Update langflow-base dependency for pre-release
if: ${{ inputs.pre_release }}
run: |
BASE_VERSION="${{ needs.determine-base-version.outputs.version }}"
echo "Base version for pre-release: $BASE_VERSION"
# Extract current langflow-base constraint from pyproject.toml
CURRENT_CONSTRAINT=$(grep -E '^\s*"langflow-base' pyproject.toml | head -1)
echo "Current constraint: $CURRENT_CONSTRAINT"
# Extract the major.minor version (e.g., "0.8" from "~=0.8.0")
MAJOR_MINOR=$(echo "$CURRENT_CONSTRAINT" | sed -E 's/.*[~>=<]+([0-9]+\.[0-9]+).*/\1/')
NEXT_MAJOR=$((${MAJOR_MINOR%.*} + 1))
# Create new constraint: >=BASE_VERSION,<NEXT_MAJOR.dev0 (preserve [complete] extra)
NEW_CONSTRAINT="\"langflow-base[complete]>=$BASE_VERSION,<$NEXT_MAJOR.dev0\""
echo "New constraint: $NEW_CONSTRAINT"
# Replace the constraint
sed -i.bak "s|\"langflow-base[^\"]*\"|$NEW_CONSTRAINT|" pyproject.toml
# Verify the change
echo "Updated dependency:"
grep "langflow-base" pyproject.toml
- name: Build project for pre-release distribution
if: ${{ inputs.pre_release }}
run: make build pre=true args="--no-sources --wheel"
- name: Build project for distribution
if: ${{ !inputs.pre_release }}
run: make build main=true args="--no-sources --wheel"
- name: Verify built version
run: |
EXPECTED_VERSION="${{ needs.determine-main-version.outputs.version }}"
WHEEL_FILE=$(ls dist/*.whl)
echo "Built wheel: $WHEEL_FILE"
NORMALIZED_VERSION=$(echo "$EXPECTED_VERSION" | sed 's/\.rc/rc/g; s/\.a/a/g; s/\.b/b/g; s/\.dev/dev/g')
echo "Expected version: $EXPECTED_VERSION"
echo "Normalized for wheel: $NORMALIZED_VERSION"
if [[ ! "$WHEEL_FILE" =~ $NORMALIZED_VERSION ]]; then
echo "❌ Error: Wheel version doesn't match expected version"
echo "Expected: $EXPECTED_VERSION (normalized: $NORMALIZED_VERSION)"
echo "Wheel file: $WHEEL_FILE"
exit 1
fi
echo "✅ Version verified: $EXPECTED_VERSION"
- name: Test CLI
run: |
uv pip install dist/*.whl
uv run python -m langflow run --host localhost --port 7860 --backend-only &
SERVER_PID=$!
# Wait for the server to start
timeout 120 bash -c 'until curl -f http://localhost:7860/health_check; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1)
# Terminate the server
kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1)
sleep 20 # give the server some time to terminate
# Check if the server is still running
if kill -0 $SERVER_PID 2>/dev/null; then
echo "Failed to terminate the server"
exit 0
else
echo "Server terminated successfully"
fi
# PyPI publishing moved to after cross-platform testing
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: dist-main
path: dist
test-cross-platform:
name: Test Cross-Platform Installation
needs: [build-base, build-main, build-lfx, build-sdk]
if: |
always() &&
!cancelled() &&
(needs.build-base.result == 'success' || needs.build-main.result == 'success' || needs.build-lfx.result == 'success')
uses: ./.github/workflows/cross-platform-test.yml
with:
base-artifact-name: "dist-base"
main-artifact-name: "dist-main"
lfx-artifact-name: "dist-lfx"
sdk-artifact-name: ${{ needs.build-sdk.result == 'success' && 'dist-sdk' || '' }}
pre_release: ${{ inputs.pre_release }}
publish-base:
name: Publish Langflow Base to PyPI
if: |
always() &&
!cancelled() &&
inputs.release_package_base &&
needs.build-base.result == 'success' &&
needs.test-cross-platform.result == 'success' &&
needs.ci.result == 'success' &&
(needs.publish-lfx.result == 'success' || needs.publish-lfx.result == 'skipped')
needs: [build-base, test-cross-platform, ci, publish-lfx]
runs-on: ubuntu-latest
steps:
- name: Download base artifact
uses: actions/download-artifact@v7
with:
name: dist-base
path: src/backend/base/dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: "3.13"
- name: Publish base to PyPI
if: ${{ !inputs.dry_run }}
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
cd src/backend/base && uv publish dist/*.whl
publish-main:
name: Publish Langflow Main to PyPI
if: ${{ inputs.release_package_main }}
needs: [build-main, test-cross-platform, publish-base, ci]
runs-on: ubuntu-latest
steps:
- name: Download main artifact
uses: actions/download-artifact@v7
with:
name: dist-main
path: dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: "3.13"
- name: Publish main to PyPI
if: ${{ !inputs.dry_run }}
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
uv publish dist/*.whl
publish-sdk:
name: Publish langflow-sdk to PyPI
needs: [build-sdk, test-cross-platform, ci]
if: |
always() &&
!cancelled() &&
inputs.release_sdk &&
needs.build-sdk.result == 'success' &&
needs.ci.result == 'success' &&
(needs.test-cross-platform.result == 'success' || needs.test-cross-platform.result == 'skipped')
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Download SDK artifact
uses: actions/download-artifact@v7
with:
name: dist-sdk
path: src/sdk/dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: "3.13"
- name: Publish SDK to PyPI
if: ${{ !inputs.dry_run }}
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
make sdk_publish
publish-lfx:
name: Publish LFX to PyPI
needs: [build-lfx, test-cross-platform, ci, publish-sdk]
if: |
always() &&
!cancelled() &&
inputs.release_lfx &&
needs.build-lfx.result == 'success' &&
needs.test-cross-platform.result == 'success' &&
needs.ci.result == 'success' &&
(needs.publish-sdk.result == 'success' || needs.publish-sdk.result == 'skipped')
runs-on: ubuntu-latest
steps:
- name: Download LFX artifact
uses: actions/download-artifact@v7
with:
name: dist-lfx
path: src/lfx/dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: "3.13"
- name: Publish LFX to PyPI
if: ${{ !inputs.dry_run }}
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
cd src/lfx && uv publish dist/*.whl
call_docker_build_base:
name: Call Docker Build Workflow for Langflow Base
if: ${{ inputs.build_docker_base }}
needs: [ci]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: base
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main:
name: Call Docker Build Workflow for Langflow
if: ${{ inputs.build_docker_main }}
needs: [ci]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main_backend:
name: Call Docker Build Workflow for Langflow Backend
if: ${{ inputs.build_docker_main && !inputs.dry_run }}
needs: [call_docker_build_main]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main-backend
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main_frontend:
name: Call Docker Build Workflow for Langflow Frontend
if: ${{ inputs.build_docker_main && !inputs.dry_run }}
needs: [call_docker_build_main]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main-frontend
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main_ep:
name: Call Docker Build Workflow for Langflow with Entrypoint
if: ${{ inputs.build_docker_main }}
needs: [ci]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main-ep
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main_all:
name: Call Docker Build Workflow for langflow-all
if: ${{ inputs.build_docker_main }}
needs: [ci]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main-all
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
create_release:
name: Create Release
runs-on: ubuntu-latest
needs:
[determine-main-version, build-main, publish-main, validate-tag-format]
if: |
always() &&
!cancelled() &&
!inputs.dry_run &&
inputs.create_release &&
needs.build-main.result == 'success' &&
needs.publish-main.result == 'success' &&
needs.validate-tag-format.result == 'success'
steps:
- uses: actions/download-artifact@v4
with:
name: dist-main
path: dist
- name: Create Release
uses: ncipollo/release-action@v1
with:
artifacts: dist/*
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
generateReleaseNotes: true
prerelease: ${{ inputs.pre_release }}
tag: ${{ needs.determine-main-version.outputs.version }}
allowUpdates: true
updateOnlyUnreleased: false