name: Nightly Build

on:
  workflow_dispatch:
    inputs:
      runs_on:
        description: "Runner to use for tests (use self-hosted for safe/release code)"
        required: false
        type: choice
        options:
          - ubuntu-latest
          - self-hosted
          - '["self-hosted", "linux", "ARM64", "langflow-ai-arm64-40gb-ephemeral"]'
        default: ubuntu-latest
      skip_frontend_tests:
        description: "Skip frontend tests. Only do this for testing purposes."
        required: false
        type: boolean
        default: false
      skip_backend_tests:
        description: "Skip backend tests. Only do this for testing purposes."
        required: false
        type: boolean
        default: false
      skip_slack:
        description: "Skip slack message. Only do this for testing purposes."
        required: false
        type: boolean
        default: false
      push_to_registry:
        description: "Whether to push images to registries. Set to false for testing builds without publishing."
        required: false
        type: boolean
        default: true
  schedule:
    # Run job at 00:00 UTC (4:00 PM PST / 5:00 PM PDT)
    - cron: "0 0 * * *"

env:
  POETRY_VERSION: "1.8.3"
  PYTHON_VERSION: "3.13"

jobs:
  validate-inputs:
    runs-on: ubuntu-latest
    steps:
      - name: Validate inputs
        if: inputs.push_to_registry && (inputs.skip_frontend_tests || inputs.skip_backend_tests)
        run: |
          echo "Cannot skip tests while push_to_registry is true."
          exit 1
  resolve-release-branch:
    runs-on: ubuntu-latest
    outputs:
      branch: ${{ steps.get_branch.outputs.branch }}
    steps:
      - name: Find latest release branch
        id: get_branch
        run: |
          git ls-remote --heads https://github.com/${{ github.repository }} 'refs/heads/release-*' \
          | awk '{print $2}' \
          | sed 's|refs/heads/||' \
          | grep -E '^release-[0-9]+\.[0-9]+\.[0-9]+$' \
          | sort -V \
          | tail -n 1 > branch.txt

          BRANCH=$(cat branch.txt)
          if [ -z "$BRANCH" ]; then
            echo "No release-* branch found in ${{ github.repository }}"
            exit 1
          fi
           echo "branch=$BRANCH" >> $GITHUB_OUTPUT
           echo "Using release branch: $BRANCH"
  create-nightly-tag:
    if: github.repository == 'langflow-ai/langflow'
    needs: [validate-inputs, resolve-release-branch]
    runs-on: ubuntu-latest
    defaults:
      run:
        shell: bash -ex -o pipefail {0}
    permissions:
      # Required to create tag
      contents: write
    outputs:
      release_tag: ${{ steps.generate_shared_tag.outputs.release_tag }}
      base_tag: ${{ steps.set_base_tag.outputs.base_tag }}
      lfx_tag: ${{ steps.generate_lfx_tag.outputs.lfx_tag }}
      sdk_tag: ${{ steps.generate_sdk_tag.outputs.sdk_tag }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          ref: ${{ needs.resolve-release-branch.outputs.branch }}
          persist-credentials: true
      - name: Confirm branch
        run: git branch --show-current
      - name: "Setup Environment"
        uses: astral-sh/setup-uv@v6
        with:
          enable-cache: true
          cache-dependency-glob: "uv.lock"
          python-version: ${{ env.PYTHON_VERSION }}
          prune-cache: false
      - name: Install the project
        run: uv sync
      - name: Check script location
        run: |
          echo "PWD: $(pwd)"
          find . -name "pypi_nightly_tag.py"
      - name: Generate shared nightly tag (main == base, lockstep)
        id: generate_shared_tag
        run: |
          # langflow-nightly pins langflow-base-nightly[complete]==<exact dev>, so both tags
          # MUST be identical. Compute them in ONE invocation (single PyPI snapshot) so they
          # cannot drift across separate calls.
          # NOTE: This outputs the tag with the `v` prefix.
          TAGS_OUTPUT="$(uv run ./scripts/ci/pypi_nightly_tag.py both)"
          RELEASE_TAG="$(printf '%s\n' "$TAGS_OUTPUT" | sed -n '1p')"
          BASE_TAG="$(printf '%s\n' "$TAGS_OUTPUT" | sed -n '2p')"
          if [ -z "$RELEASE_TAG" ] || [ "$RELEASE_TAG" != "$BASE_TAG" ]; then
            echo "Shared tag mismatch: release='$RELEASE_TAG' base='$BASE_TAG'. Exiting the workflow."
            exit 1
          fi
          echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
          echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
          echo "release_tag=$RELEASE_TAG base_tag=$BASE_TAG"

      - name: Delete existing tag if it exists
        id: check_release_tag
        run: |
          git fetch --tags
          git tag -d ${{ steps.generate_shared_tag.outputs.release_tag }} || true
          git push --delete origin ${{ steps.generate_shared_tag.outputs.release_tag }} || true
          echo "release_tag_exists=false" >> $GITHUB_OUTPUT

      - name: Generate LFX nightly tag
        id: generate_lfx_tag
        run: |
          # NOTE: This outputs the tag with the `v` prefix.
          LFX_TAG="$(uv run ./scripts/ci/lfx_nightly_tag.py)"
          echo "lfx_tag=$LFX_TAG" >> $GITHUB_OUTPUT
          echo "lfx_tag=$LFX_TAG"

      - name: Generate SDK nightly tag
        id: generate_sdk_tag
        run: |
          # NOTE: This outputs the tag with the `v` prefix.
          SDK_TAG="$(uv run ./scripts/ci/sdk_nightly_tag.py)"
          echo "sdk_tag=$SDK_TAG" >> $GITHUB_OUTPUT
          echo "sdk_tag=$SDK_TAG"

      - name: Commit tag
        id: commit_tag
        run: |
          # If the main tag does not exist in GH, we create the base tag from the existing codebase.

          git config --global user.email "bot-nightly-builds@langflow.org"
          git config --global user.name "Langflow Bot"

          RELEASE_TAG="${{ steps.generate_shared_tag.outputs.release_tag }}"
          BASE_TAG="${{ steps.generate_shared_tag.outputs.base_tag }}"
          LFX_TAG="${{ steps.generate_lfx_tag.outputs.lfx_tag }}"
          SDK_TAG="${{ steps.generate_sdk_tag.outputs.sdk_tag }}"
          echo "Updating SDK project version to $SDK_TAG"
          uv run --no-sync ./scripts/ci/update_sdk_version.py $SDK_TAG
          echo "Updating LFX project version to $LFX_TAG"
          uv run --no-sync ./scripts/ci/update_lfx_version.py $LFX_TAG $SDK_TAG
          echo "Updating base project version to $BASE_TAG and updating main project version to $RELEASE_TAG"
          uv run --no-sync ./scripts/ci/update_pyproject_combined.py main $RELEASE_TAG $BASE_TAG $LFX_TAG

          uv lock
          cd src/backend/base && uv lock && cd ../../..
          cd src/lfx && uv lock && cd ../..

          git add pyproject.toml src/backend/base/pyproject.toml src/lfx/pyproject.toml src/sdk/pyproject.toml uv.lock src/backend/base/uv.lock
          # update_lfx_version.py re-pins each bundle's `lfx` dep to
          # lfx-nightly==<dev>, so any modified bundle pyprojects need to
          # ride the same commit/tag as the rest of the version bumps.
          if compgen -G "src/bundles/*/pyproject.toml" > /dev/null; then
            git add src/bundles/*/pyproject.toml
          fi
          git commit -m "Update version and project name"

          echo "Tagging main with $RELEASE_TAG"
          if ! git tag -a $RELEASE_TAG -m "Langflow nightly $RELEASE_TAG"; then
            echo "Tag creation failed. Exiting the workflow."
            exit 1
          fi

          echo "Pushing main tag $RELEASE_TAG"
          if ! git push origin $RELEASE_TAG; then
            echo "Tag push failed. Check if the tag already exists. Exiting the workflow."
            exit 1
          fi
          # TODO: notify on failure

      - name: Checkout main nightly tag
        uses: actions/checkout@v6
        with:
          ref: ${{ steps.generate_shared_tag.outputs.release_tag }}
          persist-credentials: true

      - name: Retrieve Base Tag
        id: retrieve_base_tag
        working-directory: src/backend/base
        run: |
          # If the main tag already exists, we need to retrieve the base version from the main tag codebase.
          version=$(uv tree | grep 'langflow-base' | awk '{print $3}' | head -n 1)
          echo "base_tag=$version" >> $GITHUB_OUTPUT
          echo "base_tag=$version"

      - name: Set Base Tag
        id: set_base_tag
        run: |
          if [ "${{ steps.retrieve_base_tag.conclusion }}" != "skipped" ] && [ "${{ steps.retrieve_base_tag.outputs.base_tag }}" ]; then
            BASE_TAG="${{ steps.retrieve_base_tag.outputs.base_tag }}"
            echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
            echo "base_tag=$BASE_TAG"
          elif [ "${{ steps.commit_tag.conclusion }}" != "skipped" ] && [ "${{ steps.generate_shared_tag.outputs.base_tag }}" ]; then
            BASE_TAG="${{ steps.generate_shared_tag.outputs.base_tag }}"
            echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
            echo "base_tag=$BASE_TAG"
          else
            echo "No base tag found. Exiting the workflow."
            exit 1
          fi

  frontend-tests-linux:
    if: github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests
    name: Run Frontend Tests - Linux
    needs: [resolve-release-branch, create-nightly-tag]
    uses: ./.github/workflows/typescript_test.yml
    with:
      tests_folder: "tests"
      release: true
      runs-on: ubuntu-latest
      ref: ${{ needs.resolve-release-branch.outputs.branch }}
    secrets:
      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      STORE_API_KEY: ${{ secrets.STORE_API_KEY }}
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}

  frontend-tests-windows:
    if: github.repository == 'langflow-ai/langflow' && !inputs.skip_frontend_tests
    name: Run Frontend Tests - Windows
    needs: [resolve-release-branch, create-nightly-tag]
    # Windows tests are non-blocking - the release-nightly-build job only checks
    # frontend-tests-linux.result, allowing Windows tests to fail without blocking the build.
    # This gives us visibility into Windows-specific issues while we stabilize the tests.
    uses: ./.github/workflows/typescript_test.yml
    with:
      tests_folder: "tests"
      release: true
      runs-on: windows-latest
      ref: ${{ needs.resolve-release-branch.outputs.branch }}
    secrets:
      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      STORE_API_KEY: ${{ secrets.STORE_API_KEY }}
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
      TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}

  backend-unit-tests:
    if: github.repository == 'langflow-ai/langflow' && !inputs.skip_backend_tests
    name: Run Backend Unit Tests
    needs: [resolve-release-branch, create-nightly-tag]
    uses: ./.github/workflows/python_test.yml
    with:
      python-versions: '["3.10", "3.11", "3.12", "3.13", "3.14"]'
      runs-on: ${{ inputs['runs_on'] || github.event.inputs['runs_on'] || 'ubuntu-latest' }}
      ref: ${{ needs.resolve-release-branch.outputs.branch }}
    secrets:
      OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
      ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}

  # Not making nightly builds dependent on integration test success
  # due to inherent flakiness of 3rd party integrations
  # Revisit when https://github.com/langflow-ai/langflow/pull/3607 is merged.
  # backend-integration-tests:
  #   name: Run Backend Integration Tests
  #   needs: create-nightly-tag
  #   uses: ./.github/workflows/integration_tests.yml
  #   with:
  #     python-versions: '["3.10", "3.11", "3.12", "3.13"]'
  #     ref: ${{ needs.create-nightly-tag.outputs.tag }}

  release-nightly-build:
    if: github.repository == 'langflow-ai/langflow' && always() && needs.create-nightly-tag.result == 'success' && needs.frontend-tests-linux.result != 'failure' && needs.backend-unit-tests.result != 'failure'
    name: Run Nightly Langflow Build
    needs:
      [validate-inputs, create-nightly-tag, frontend-tests-linux, frontend-tests-windows, backend-unit-tests]
    uses: ./.github/workflows/release_nightly.yml
    with:
      build_docker_base: true
      build_docker_main: true
      build_docker_ep: true
      build_lfx: true
      nightly_tag_release: ${{ needs.create-nightly-tag.outputs.release_tag }}
      nightly_tag_base: ${{ needs.create-nightly-tag.outputs.base_tag }}
      nightly_tag_lfx: ${{ needs.create-nightly-tag.outputs.lfx_tag }}
      nightly_tag_sdk: ${{ needs.create-nightly-tag.outputs.sdk_tag }}
      # When triggered by schedule, inputs.push_to_registry is not set, so default to true
      # When triggered manually, use the provided value (default is also true)
      push_to_registry: ${{ github.event_name == 'schedule' || inputs.push_to_registry != false }}
    secrets: inherit
  db-migration-validation:
    name: Run DB Migration Validation
    needs: [create-nightly-tag, release-nightly-build]
    if: ${{ github.repository == 'langflow-ai/langflow' && always() && needs.release-nightly-build.result == 'success' }}
    uses: ./.github/workflows/db-migration-validation.yml
    with:
      # Pass Docker image reference for docker-compose and version without 'v' prefix for pip
      # release_tag format: v1.10.0.dev20260522
      # We need: langflowai/langflow-nightly:1.10.0.dev20260522 for Docker
      nightly_tag: langflowai/langflow-nightly:${{ needs.create-nightly-tag.outputs.release_tag }}
    secrets: inherit

  slack-notification:
    name: Send Slack Notification
    needs: [frontend-tests-linux, frontend-tests-windows, backend-unit-tests, release-nightly-build, db-migration-validation]
    if: ${{ github.repository == 'langflow-ai/langflow' && !inputs.skip_slack && always() && (needs.release-nightly-build.result == 'failure' || needs.frontend-tests-linux.result == 'failure' || needs.frontend-tests-windows.result == 'failure' || needs.backend-unit-tests.result == 'failure' || needs.db-migration-validation.result == 'failure' || needs.release-nightly-build.result == 'success') }}
    runs-on: ubuntu-latest
    steps:
      - name: Send failure notification to Slack
        if: ${{ needs.release-nightly-build.result == 'failure' || needs.frontend-tests-linux.result == 'failure' || needs.frontend-tests-windows.result == 'failure' || needs.backend-unit-tests.result == 'failure' || needs.db-migration-validation.result == 'failure' }}
        run: |
          # Determine which job failed
          FAILED_JOB="unknown"
          if [ "${{ needs.release-nightly-build.result }}" == "failure" ]; then
            FAILED_JOB="release-nightly-build"
          elif [ "${{ needs.frontend-tests-linux.result }}" == "failure" ]; then
            FAILED_JOB="frontend-tests-linux"
          elif [ "${{ needs.frontend-tests-windows.result }}" == "failure" ]; then
            FAILED_JOB="frontend-tests-windows (non-blocking)"
          elif [ "${{ needs.backend-unit-tests.result }}" == "failure" ]; then
            FAILED_JOB="backend-unit-tests"
          elif [ "${{ needs.db-migration-validation.result }}" == "failure" ]; then
            FAILED_JOB="db-migration-validation"
          fi

          curl -X POST -H 'Content-type: application/json' \
          --data "{
            \"blocks\": [
              {
                \"type\": \"section\",
                \"text\": {
                  \"type\": \"mrkdwn\",
                  \"text\": \"❌ *Nightly Build Failed*\"
                }
              },
              {
                \"type\": \"section\",
                \"fields\": [
                  {
                    \"type\": \"mrkdwn\",
                    \"text\": \"*Failed Job:*\\n$FAILED_JOB\"
                  },
                  {
                    \"type\": \"mrkdwn\",
                    \"text\": \"*Status:*\\nfailure\"
                  }
                ]
              },
              {
                \"type\": \"section\",
                \"text\": {
                  \"type\": \"mrkdwn\",
                  \"text\": \"*Note:* Frontend tests now run on both Linux and Windows\"
                }
              },
              {
                \"type\": \"context\",
                \"elements\": [
                  {
                    \"type\": \"mrkdwn\",
                    \"text\": \"<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View full logs on GitHub>\"
                  }
                ]
              }
            ]
          }" ${{ secrets.LANGFLOW_ENG_SLACK_WEBHOOK_URL }}

      - name: Send success notification to Slack
        if: ${{ !inputs.skip_slack && needs.release-nightly-build.result == 'success' && needs.db-migration-validation.result != 'failure' }}
        run: |
          curl -X POST -H 'Content-type: application/json' \
          --data '{
            "blocks": [
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "✅ *Nightly Build Successful*"
                }
              },
              {
                "type": "section",
                "fields": [
                  {
                    "type": "mrkdwn",
                    "text": "*Job:*\nrelease-nightly-build"
                  },
                  {
                    "type": "mrkdwn",
                    "text": "*Status:*\n`${{ needs.release-nightly-build.result }}`"
                  }
                ]
              },
              {
                "type": "section",
                "text": {
                  "type": "mrkdwn",
                  "text": "*Platforms:* Linux & Windows Playwright tests passed"
                }
              },
              {
                "type": "context",
                "elements": [
                  {
                    "type": "mrkdwn",
                    "text": "<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View full logs on GitHub>"
                  }
                ]
              }
            ]
          }' ${{ secrets.LANGFLOW_ENG_SLACK_WEBHOOK_URL }}
