Files
langflow/.github/workflows/release.yml
Janardan Singh Kavia 38d142a723 fix: Upgrade cuga to 0.2.20 to resolve playwright dependency conflict (#12703)
* fix: upgrade playwright to 1.58.0 to address Chromium CVEs

- Add playwright>=1.58.0 to override-dependencies in pyproject.toml
- Update uv.lock: playwright 1.49.0 -> 1.58.0, pyee 12.0.0 -> 13.0.1
- Fixes CVE-2026-2313, CVE-2026-2314, CVE-2026-2315, CVE-2026-2319,
  CVE-2026-2321, CVE-2026-2441, CVE-2026-2648, CVE-2026-2649
- Ensures Docker builds download updated Chromium with security patches

* fix: update npm to latest version to address brace-expansion CVE-2026-33750

- Add npm update after Node.js installation in Dockerfile
- Fixes CVE-2026-33750 in system npm's brace-expansion dependency
- System npm had brace-expansion 2.0.2, update gets 5.0.5+
- Low risk change: npm is backward compatible, only affects CLI tool

* revert: remove npm update from Dockerfile

- npm update attempts were causing CI build failures
- Bundled npm has issues but updating it is proving problematic
- Focus on playwright CVE fix which is the primary concern
- brace-expansion CVE-2026-33750 is lower priority (DoS only)

* chore: sync uv.lock files

sync uv.lock files

* fix(mcp): dedupe edges in connect_components (#12701)

* fix(mcp): make add_connection idempotent to avoid duplicate edges

connect_components used to append a new edge unconditionally. Because
the edge id is deterministic from source/target/handles, calling it for
a pair the flow already had wired up (UI-then-MCP, batch retry, or just
a repeat call) produced a second edge with the same id, double-wiring
the flow at runtime.

Before appending, scan the existing edges for one with the same id and
return that instead. Different outputs/inputs between the same pair
still produce distinct ids and remain supported.

* test(mcp): cover dedupe against UI-saved edges, broaden match key

Older Langflow UIs saved edges with an `xy-edge__` id prefix instead of
the current `reactflow__edge-`, so an id-based dedup would miss the
UI-then-MCP case for any flow that came from an older version. Switch
the existence check to a structural one (source, target, sourceHandle
name, targetHandle fieldName) so the same logical connection dedupes
regardless of id format.

Add a fixture-driven test that loads MemoryChatbotNoLLM.json (an
xy-edge-prefixed flow) and replays each connection through
add_connection, asserting the edge count does not grow.

* fix(mcp): validate_flow fast-fails and reports partial errors (#12697)

* fix(mcp): validate_flow fast-fails and reports partial errors

validate_flow polled /monitor/builds for up to 30 seconds waiting for
every component to finish before reporting errors. When a component
fails early (for example a missing required field), downstream
components never run, so the loop waited out the full window and
returned just "Build timed out: N/M components completed" with no
actionable context.

- Short-circuit as soon as any completed build reports valid: false;
  return those errors immediately instead of polling on.
- On timeout, include the errors from the builds that did complete
  plus a component_count so the caller can see progress.
- Extract _collect_build_errors so the poll loop and timeout branch
  share the same error shape.

* fix(mcp): stream validate_flow build inline instead of polling

The previous implementation triggered an async build and polled
/monitor/builds, which depended on FastAPI BackgroundTasks firing the
log_vertex_build calls after the trigger request had returned. Under
ASGI test transport these tasks never run, so /monitor/builds stayed
empty and validate_flow timed out with component_count=0.

Switch to event_delivery=direct so the build streams its events back
inside the same request:

- Drive the build via client.stream_post and aggregate per-vertex
  results from end_vertex events.
- Fast-fail on the first vertex with valid=false, since downstream
  vertices depend on it and would not produce useful information.
- Surface top-level error events as a single flow-level error.
- Replace _collect_build_errors with _extract_vertex_error, which
  reads the structured error payload from the end_vertex outputs.

Update the lfx unit tests to use the streaming shape and tighten the
backend integration test to assert real success now that the build
actually runs end to end under ASGI.

* fix(graph): make end_all_traces_in_context Python 3.10 compatible

The implementation called asyncio.create_task(coro, context=context),
but the context= keyword was added to create_task in Python 3.11. On
3.10 it raised TypeError. The bug was latent because nothing in the
test suite previously consumed a streaming build response far enough
for Starlette to dispatch the post-response BackgroundTasks where this
code lives. The validate_flow streaming change exposes it.

On 3.10, route the create_task call through context.run so the new
Task copies the captured context as its current context, matching the
isolation the 3.11 path provides via the context= kwarg.

Add a regression test that asserts end_all_traces sees the value of a
contextvar set before the context was captured, even after the caller
mutates that var.

* fix: failing wxo list llm test (#12700)

patch service layer and update failing test

* [autofix.ci] apply automated fixes

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

* Try to fix the missing typer import

---------

Co-authored-by: Janardan S Kavia <janardanskavia@Janardans-MacBook-Pro.local>
Co-authored-by: Adam Aghili <Adam.Aghili@ibm.com>
Co-authored-by: Gabriel Luiz Freitas Almeida <gabriel@logspace.ai>
Co-authored-by: Hamza Rashid <74062092+HzaRashid@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Eric Hare <ericrhare@gmail.com>
2026-04-14 21:23:12 +00:00

1027 lines
38 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
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-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']"
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
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
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]
if: |
always() &&
!cancelled() &&
!inputs.dry_run &&
inputs.create_release &&
needs.build-main.result == 'success' &&
needs.publish-main.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