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 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 "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.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 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' | 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 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-lfx: name: Build LFX needs: [determine-lfx-version] if: ${{ 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: 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: 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 uv pip install dist/*.whl --force-reinstall uv run 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, determine-base-version, determine-lfx-version] if: ${{ inputs.release_package_base && needs.determine-base-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.12" prune-cache: false - name: Download LFX artifact uses: actions/download-artifact@v7 with: name: dist-lfx path: ./lfx-dist - name: Install dependencies with local LFX wheel run: | # Create virtual environment uv venv # Install using pip with local wheel directory as find-links uv pip install --find-links ./lfx-dist --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,=$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, determine-base-version, determine-main-version, determine-lfx-version, ] if: ${{ inputs.release_package_main && needs.determine-main-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.12" prune-cache: false - name: Download LFX artifact uses: actions/download-artifact@v7 with: name: dist-lfx path: ./lfx-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 uv pip install --find-links ./lfx-dist --find-links ./base-dist --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,=$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] 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" pre_release: ${{ inputs.pre_release }} publish-base: name: Publish Langflow Base to PyPI if: ${{ inputs.release_package_base }} needs: [build-base, test-cross-platform, ci] 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-lfx: name: Publish LFX to PyPI if: ${{ inputs.release_lfx }} needs: [build-lfx, test-cross-platform, ci] 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