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., "0.3" from "~=0.3.0")
          MAJOR_MINOR=$(echo "$CURRENT_CONSTRAINT" | sed -E 's/.*[~>=<]+([0-9]+\.[0-9]+).*/\1/')
          NEXT_MAJOR=$((${MAJOR_MINOR%.*} + 1))

          # Create new constraint: >=LFX_VERSION,<NEXT_MAJOR.dev0
          NEW_CONSTRAINT="\"lfx>=$LFX_VERSION,<$NEXT_MAJOR.dev0\""

          echo "New constraint: $NEW_CONSTRAINT"

          # Replace the constraint
          sed -i.bak "s|\"lfx[^\"]*\"|$NEW_CONSTRAINT|" pyproject.toml

          # Verify the change
          echo "Updated lfx dependency:"
          grep "lfx" pyproject.toml
      - name: Build project for distribution
        run: make build base=true args="--wheel"
      - name: Verify built version
        run: |
          EXPECTED_VERSION="${{ needs.determine-base-version.outputs.version }}"
          WHEEL_FILE=$(ls dist/*.whl 2>/dev/null || ls src/backend/base/dist/*.whl)
          echo "Built wheel: $WHEEL_FILE"

          NORMALIZED_VERSION=$(echo "$EXPECTED_VERSION" | sed 's/\.rc/rc/g; s/\.a/a/g; s/\.b/b/g; s/\.dev/dev/g')
          echo "Expected version: $EXPECTED_VERSION"
          echo "Normalized for wheel: $NORMALIZED_VERSION"

          if [[ ! "$WHEEL_FILE" =~ $NORMALIZED_VERSION ]]; then
            echo "❌ Error: Wheel version doesn't match expected version"
            echo "Expected: $EXPECTED_VERSION (normalized: $NORMALIZED_VERSION)"
            echo "Wheel file: $WHEEL_FILE"
            exit 1
          fi
          echo "✅ Version verified: $EXPECTED_VERSION"
      - name: Test CLI
        run: |
          # TODO: Unsure why the whl is not built in src/backend/base/dist
          mkdir src/backend/base/dist
          mv dist/*.whl src/backend/base/dist
          uv pip install src/backend/base/dist/*.whl
          uv run python -m langflow run --host localhost --port 7860 --backend-only &
          SERVER_PID=$!
          # Wait for the server to start
          timeout 120 bash -c 'until curl -f http://localhost:7860/api/v1/auto_login; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1)
          # Terminate the server
          kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1)
          sleep 20 # give the server some time to terminate
          # Check if the server is still running
          if kill -0 $SERVER_PID 2>/dev/null; then
            echo "Failed to terminate the server"
            exit 0
          else
            echo "Server terminated successfully"
          fi

      # PyPI publishing moved to after cross-platform testing

      - name: Upload Artifact
        uses: actions/upload-artifact@v6
        with:
          name: dist-base
          path: src/backend/base/dist

  build-main:
    name: Build Langflow Main
    needs:
      [
        build-base,
        build-lfx,
        build-sdk,
        determine-base-version,
        determine-main-version,
        determine-lfx-version,
      ]
    if: |
      always() &&
      !cancelled() &&
      inputs.release_package_main &&
      needs.determine-main-version.result == 'success' &&
      needs.determine-main-version.outputs.skipped == 'false' &&
      needs.build-base.result == 'success' &&
      needs.build-lfx.result == 'success'
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.release_tag }}
      - name: Setup Environment
        uses: astral-sh/setup-uv@v6
        with:
          enable-cache: true
          cache-dependency-glob: "uv.lock"
          python-version: "3.12"
          prune-cache: false
      - name: Download LFX artifact
        uses: actions/download-artifact@v7
        with:
          name: dist-lfx
          path: ./lfx-dist
      - name: Download SDK artifact
        if: needs.build-sdk.result == 'success'
        uses: actions/download-artifact@v7
        with:
          name: dist-sdk
          path: ./sdk-dist
      - name: Download base artifact
        uses: actions/download-artifact@v7
        with:
          name: dist-base
          path: ./base-dist
      - name: Install dependencies with local wheels
        run: |
          # Create virtual environment
          uv venv
          # Install using pip with local wheel directories as find-links
          FIND_LINKS="--find-links ./lfx-dist --find-links ./base-dist"
          if [ -d "./sdk-dist" ]; then
            FIND_LINKS="$FIND_LINKS --find-links ./sdk-dist"
          fi
          uv pip install $FIND_LINKS --prerelease=allow -e .
      - name: Check for dependency incompatibility
        if: ${{ inputs.run_pip_check }}
        run: uv pip check
      - name: Set version for pre-release
        if: ${{ inputs.pre_release }}
        run: |
          VERSION="${{ needs.determine-main-version.outputs.version }}"
          echo "Setting main pre-release version to: $VERSION"

          # Update version in main pyproject.toml
          sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml

          # Verify the change
          echo "Updated pyproject.toml version:"
          grep "^version" pyproject.toml
      - name: Update langflow-base dependency for pre-release
        if: ${{ inputs.pre_release }}
        run: |
          BASE_VERSION="${{ needs.determine-base-version.outputs.version }}"
          echo "Base version for pre-release: $BASE_VERSION"

          # Extract current langflow-base constraint from pyproject.toml
          CURRENT_CONSTRAINT=$(grep -E '^\s*"langflow-base' pyproject.toml | head -1)
          echo "Current constraint: $CURRENT_CONSTRAINT"

          # Extract the major.minor version (e.g., "0.8" from "~=0.8.0")
          MAJOR_MINOR=$(echo "$CURRENT_CONSTRAINT" | sed -E 's/.*[~>=<]+([0-9]+\.[0-9]+).*/\1/')
          NEXT_MAJOR=$((${MAJOR_MINOR%.*} + 1))

          # Create new constraint: >=BASE_VERSION,<NEXT_MAJOR.dev0 (preserve [complete] extra)
          NEW_CONSTRAINT="\"langflow-base[complete]>=$BASE_VERSION,<$NEXT_MAJOR.dev0\""

          echo "New constraint: $NEW_CONSTRAINT"

          # Replace the constraint
          sed -i.bak "s|\"langflow-base[^\"]*\"|$NEW_CONSTRAINT|" pyproject.toml

          # Verify the change
          echo "Updated dependency:"
          grep "langflow-base" pyproject.toml
      - name: Build project for pre-release distribution
        if: ${{ inputs.pre_release }}
        run: make build pre=true args="--no-sources --wheel"
      - name: Build project for distribution
        if: ${{ !inputs.pre_release }}
        run: make build main=true args="--no-sources --wheel"
      - name: Verify built version
        run: |
          EXPECTED_VERSION="${{ needs.determine-main-version.outputs.version }}"
          WHEEL_FILE=$(ls dist/*.whl)
          echo "Built wheel: $WHEEL_FILE"

          NORMALIZED_VERSION=$(echo "$EXPECTED_VERSION" | sed 's/\.rc/rc/g; s/\.a/a/g; s/\.b/b/g; s/\.dev/dev/g')
          echo "Expected version: $EXPECTED_VERSION"
          echo "Normalized for wheel: $NORMALIZED_VERSION"

          if [[ ! "$WHEEL_FILE" =~ $NORMALIZED_VERSION ]]; then
            echo "❌ Error: Wheel version doesn't match expected version"
            echo "Expected: $EXPECTED_VERSION (normalized: $NORMALIZED_VERSION)"
            echo "Wheel file: $WHEEL_FILE"
            exit 1
          fi
          echo "✅ Version verified: $EXPECTED_VERSION"
      - name: Test CLI
        run: |
          uv pip install dist/*.whl
          uv run python -m langflow run --host localhost --port 7860 --backend-only &
          SERVER_PID=$!
          # Wait for the server to start
          timeout 120 bash -c 'until curl -f http://localhost:7860/health_check; do sleep 2; done' || (echo "Server did not start in time" && kill $SERVER_PID && exit 1)
          # Terminate the server
          kill $SERVER_PID || (echo "Failed to terminate the server" && exit 1)
          sleep 20 # give the server some time to terminate
          # Check if the server is still running
          if kill -0 $SERVER_PID 2>/dev/null; then
            echo "Failed to terminate the server"
            exit 0
          else
            echo "Server terminated successfully"
          fi

      # PyPI publishing moved to after cross-platform testing

      - name: Upload Artifact
        uses: actions/upload-artifact@v6
        with:
          name: dist-main
          path: dist

  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: 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
          for wheel in "${wheels[@]}"; do
            echo "Publishing $wheel"
            uv publish "$wheel"
          done

  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
      - 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
