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 release_bundles: description: "Release every src/bundles/* package (required when releasing main, since main pins exact bundle versions)" 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 "release_bundles: ${{ inputs.release_bundles }}" 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 if [ "${{ inputs.release_package_main }}" = "true" ] && [ "${{ inputs.release_bundles }}" = "false" ]; then echo "Error: Cannot release Langflow Main without releasing bundles." echo "Langflow Main pins exact bundle versions (lfx-arxiv, lfx-duckduckgo, ...). Please enable 'release_bundles' or disable 'release_package_main'." 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 -E '^langflow(-nightly)?[[:space:]]' | 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 }}" # importlib.metadata returns the PEP 440 canonical form, so normalize # the expected version the same way the wheel filename check does. NORMALIZED_VERSION=$(echo "$EXPECTED_VERSION" | sed 's/\.rc/rc/g; s/\.a/a/g; s/\.b/b/g; s/\.dev/dev/g') ./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 == '$NORMALIZED_VERSION', f'Installed version {installed_version} does not match expected version $NORMALIZED_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., "1.10" from "~=1.10.0", or from the # lower bound of the range form ">=1.10.0,<1.11.dev0"). Anchor to the FIRST # version in the line so a greedy match can't pick up the range's upper bound. MAJOR_MINOR=$(echo "$CURRENT_CONSTRAINT" | sed -E 's/^[^0-9]*([0-9]+\.[0-9]+).*/\1/') MAJOR=${MAJOR_MINOR%.*} MINOR=${MAJOR_MINOR#*.} NEXT_MINOR=$((MINOR + 1)) # Create new constraint: >=LFX_VERSION,=$LFX_VERSION,<$MAJOR.$NEXT_MINOR.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,=$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 build-bundles: name: Build Langflow Bundles needs: [build-lfx, determine-lfx-version] if: | always() && !cancelled() && inputs.release_bundles && needs.build-lfx.result == 'success' runs-on: ubuntu-latest defaults: run: shell: bash outputs: bundles-found: ${{ steps.build.outputs.bundles-found }} steps: - name: Checkout code uses: actions/checkout@v6 with: ref: ${{ inputs.release_tag }} - 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: Relax bundle lfx floor for pre-release if: ${{ inputs.pre_release }} run: | # Bundles floor lfx at the release line (e.g. lfx>=1.10.0). A pre-release # such as 1.10.0rc0 sorts BELOW 1.10.0 under PEP 440, so it fails that # floor and the cross-platform install test cannot resolve. Mirror what # build-base / build-main / build-lfx already do for pre-release: rewrite # the floor to the exact pre-release lfx version, preserving the bundle's # wide <2.0.0 BUNDLE_API cap. Stable source stays at >=1.10.0 -- only the # RC wheels are relaxed, at build time. LFX_VERSION="${{ needs.determine-lfx-version.outputs.version }}" echo "Relaxing bundle lfx floor to pre-release version: $LFX_VERSION" for pyproject in src/bundles/*/pyproject.toml; do sed -i.bak -E "s|\"lfx>=[^\"]*\"|\"lfx>=${LFX_VERSION},<2.0.0\"|" "$pyproject" rm -f "${pyproject}.bak" grep -E '"lfx>=' "$pyproject" | sed "s|^| ${pyproject}: |" done - name: Build bundle wheels id: build run: | shopt -s nullglob bundles=(src/bundles/*/pyproject.toml) if [ ${#bundles[@]} -eq 0 ]; then echo "No bundles found under src/bundles/*/" echo "bundles-found=0" >> $GITHUB_OUTPUT exit 0 fi mkdir -p bundles-dist BUNDLES_DIST="$PWD/bundles-dist" for bundle_pyproject in "${bundles[@]}"; do bundle_dir=$(dirname "$bundle_pyproject") echo "Building wheel for $bundle_dir" (cd "$bundle_dir" && uv build --wheel --out-dir "$BUNDLES_DIST") done echo "bundles-found=1" >> $GITHUB_OUTPUT ls -la bundles-dist/ - name: Upload bundles artifact if: steps.build.outputs.bundles-found == '1' uses: actions/upload-artifact@v6 with: name: dist-bundles path: bundles-dist test-cross-platform: name: Test Cross-Platform Installation needs: [build-base, build-main, build-lfx, build-sdk, build-bundles] 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' || '' }} bundles-artifact-name: ${{ needs.build-bundles.result == 'success' && needs.build-bundles.outputs.bundles-found == '1' && 'dist-bundles' || '' }} 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-bundles: name: Publish Bundles to PyPI needs: [build-bundles, test-cross-platform, ci, publish-lfx] if: | always() && !cancelled() && inputs.release_bundles && needs.build-bundles.result == 'success' && needs.build-bundles.outputs.bundles-found == '1' && needs.test-cross-platform.result == 'success' && needs.ci.result == 'success' && (needs.publish-lfx.result == 'success' || needs.publish-lfx.result == 'skipped') runs-on: ubuntu-latest steps: - name: Download bundles artifact uses: actions/download-artifact@v7 with: name: dist-bundles path: bundles-dist - name: Setup Environment uses: astral-sh/setup-uv@v6 with: enable-cache: false python-version: "3.13" - name: Publish bundles to PyPI if: ${{ !inputs.dry_run }} env: UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }} run: | shopt -s nullglob wheels=(bundles-dist/*.whl) if [ ${#wheels[@]} -eq 0 ]; then echo "No bundle wheels to publish." exit 0 fi failed=0 for wheel in "${wheels[@]}"; do echo "Publishing $wheel" # Tolerate "already exists" so reruns -- or a bundle whose version is # unchanged and already on PyPI -- do not fail the job. Matches the # duplicate-tolerant publish in release_bundles.yml. if ! err=$(uv publish "$wheel" 2>&1); then echo "$err" if echo "$err" | grep -qiE 'already exists|file already exists|HTTP 400|duplicate'; then echo "Skipping $wheel: already published." else echo "Publish failed for $wheel" failed=1 fi fi done if [ "$failed" = "1" ]; then exit 1 fi publish-main: name: Publish Langflow Main to PyPI if: | always() && !cancelled() && inputs.release_package_main && needs.build-main.result == 'success' && needs.test-cross-platform.result == 'success' && needs.publish-base.result == 'success' && needs.ci.result == 'success' && (needs.publish-bundles.result == 'success' || needs.publish-bundles.result == 'skipped') needs: [build-main, test-cross-platform, publish-base, publish-bundles, 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 # The release must attach to the dispatched v-prefixed tag. Passing the # bare version as `tag` made GitHub mint a new lightweight tag at the # default-branch HEAD — the wrong commit, still carrying the previous # version (main only adopts the new version via the post-release # back-merge). Pre-releases keep their computed tag (e.g. 1.10.0rc1), # but `commit` pins any newly minted tag to the release commit. - name: Resolve release commit id: release_commit env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | sha=$(gh api "repos/${{ github.repository }}/commits/${{ inputs.release_tag }}" --jq '.sha') echo "Release tag '${{ inputs.release_tag }}' resolves to commit $sha" echo "sha=$sha" >> "$GITHUB_OUTPUT" - name: Create Release uses: ncipollo/release-action@v1 with: artifacts: dist/* token: ${{ secrets.GITHUB_TOKEN }} draft: false generateReleaseNotes: true prerelease: ${{ inputs.pre_release }} tag: ${{ inputs.pre_release && needs.determine-main-version.outputs.version || inputs.release_tag }} name: ${{ needs.determine-main-version.outputs.version }} commit: ${{ steps.release_commit.outputs.sha }} allowUpdates: true updateOnlyUnreleased: false