name: CI

on:
  workflow_call:
    inputs:
      ref:
        description: "(Optional) Ref to checkout"
        required: false
        type: string
      python-versions:
        description: "Python Versions"
        required: false
        type: string
        default: "['3.10', '3.14']"
      frontend-tests-folder:
        description: "Frontend Tests Folder"
        required: false
        type: string
        default: "tests/core"
      release:
        description: "Release"
        required: false
        type: boolean
        default: false
      run-all-tests:
        description: "Run all tests regardless of file changes (skips path filtering)"
        required: false
        type: boolean
        default: false
      runs-on:
        description: "Runner to use for the tests"
        required: false
        type: string
        default: "ubuntu-latest"
  workflow_dispatch:
    inputs:
      ref:
        description: "(Optional) Ref to checkout"
        required: false
        type: string
      openai_api_key:
        description: "OpenAI API Key"
        required: false
        type: string
      store_api_key:
        description: "Store API Key"
        required: false
        type: string
      python-versions:
        description: "Python Versions"
        required: false
        type: string
        default: "['3.10', '3.14']"
      runs-on:
        description: "Runner to use for the tests"
        required: false
        type: choice
        options:
          - ubuntu-latest
          - self-hosted
          - '["self-hosted", "linux", "ARM64", "langflow-ai-arm64-40gb-ephemeral"]'
        default: ubuntu-latest
  pull_request:
    types: [opened, synchronize, labeled]
  merge_group:
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

env:
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
  STORE_API_KEY: ${{ secrets.STORE_API_KEY }}
  TAVILY_API_KEY: ${{ secrets.TAVILY_API_KEY }}

jobs:
  echo-inputs:
    name: Echo Inputs
    runs-on: ubuntu-latest
    steps:
      - name: Echo inputs
        run: |
          echo "Inputs:"
          echo "  ref: ${{ inputs.ref }}"
          echo "  python-versions: ${{ inputs.python-versions }}"
          echo "  frontend-tests-folder: ${{ inputs.frontend-tests-folder }}"
          echo "  release: ${{ inputs.release }}"
          echo "  run-all-tests: ${{ inputs.run-all-tests }}"
          echo "  runs-on: ${{ inputs.runs-on }}"

  validate-pr-title:
    name: Validate PR Title
    if: ${{ github.event_name == 'pull_request' }}
    runs-on: ubuntu-latest
    env:
      PR_TITLE: ${{ github.event.pull_request.title }}
    steps:
      - name: Reject titles ending in ellipsis
        run: |
          echo "PR title: $PR_TITLE"
          if [[ "$PR_TITLE" == *"..." || "$PR_TITLE" == *"…" ]]; then
            echo "❌ PR title ends with ellipsis. Please write a complete, descriptive title."
            exit 1
          fi
          echo "✅ PR title is complete."

  check-nightly-status:
    name: Check PyPI Version Update
    runs-on: ubuntu-latest
    outputs:
      should-proceed: ${{ steps.check-pypi.outputs.success }}
    steps:
      - name: Check PyPI package update
        id: check-pypi
        run: |
          # Get today's date in ISO format for comparison
          TODAY=$(date -u +"%Y-%m-%d")
          echo "Today's date: $TODAY"

          # Query PyPI API for the langflow package
          HTTP_STATUS=$(curl -s -o response.json -w "%{http_code}" https://pypi.org/pypi/langflow-nightly/json)

          # Check HTTP status code first
          if [ "$HTTP_STATUS" -ne 200 ]; then
            echo "Error: PyPI API returned HTTP status $HTTP_STATUS"
            echo "success=false" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Check if response is valid JSON before proceeding
          if ! jq -e . response.json >/dev/null 2>&1; then
            echo "Error: Invalid JSON response from PyPI API"
            echo "Response preview:"
            head -n 10 response.json
            echo "success=false" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Extract the latest version
          LATEST_VERSION=$(jq -r '.info.version // empty' response.json)

          if [ -z "$LATEST_VERSION" ]; then
            echo "Could not extract latest version"
            echo "success=false" >> $GITHUB_OUTPUT
            exit 0
          fi

          # Extract the release date of the latest version
          RELEASE_DATE=$(jq -r --arg ver "$LATEST_VERSION" '.releases[$ver][0].upload_time_iso_8601 // empty' response.json | cut -d'T' -f1)

          if [ -z "$RELEASE_DATE" ]; then
            echo "Could not extract release date"
            echo "success=false" >> $GITHUB_OUTPUT
            exit 0
          fi

          echo "Latest version: $LATEST_VERSION"
          echo "Release date: $RELEASE_DATE"

          # Check if the release date is today
          if [[ "$RELEASE_DATE" == "$TODAY" ]]; then
            echo "Package was updated today"
            echo "success=true" >> $GITHUB_OUTPUT
          else
            echo "Package was not updated today"
            echo "success=false" >> $GITHUB_OUTPUT
          fi

          # Clean up
          rm -f response.json

  set-ci-condition:
    name: Should Run CI
    runs-on: ubuntu-latest
    outputs:
      should-run-ci: ${{ github.event.pull_request.draft == false || (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' || github.event_name == 'merge_group') }}
      should-run-tests: ${{ !contains(github.event.pull_request.labels.*.name, 'fast-track') || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group' }}
    steps:
      #  Do anything just to make the job run
      - run: echo "Debug CI Condition"
      - run: echo "Labels -> ${{ join(github.event.pull_request.labels.*.name, ',') }}"
      - run: echo "IsDraft -> ${{ github.event.pull_request.draft }}"
      - run: echo "Event name -> ${{ github.event_name }}"
      - run: echo "Should run ci -> ${{ (github.event.pull_request.draft == false) || (github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_call' || github.event_name == 'merge_group') }}"
      - run: echo "Should run tests -> ${{ !contains(github.event.pull_request.labels.*.name, 'fast-track') || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' || github.event_name == 'merge_group' }}"

  path-filter:
    needs: set-ci-condition
    if: ${{ needs.set-ci-condition.outputs.should-run-ci == 'true' && !inputs.run-all-tests }}
    name: Filter Paths
    runs-on: ubuntu-latest
    outputs:
      python: ${{ steps.filter.outputs.python }}
      frontend: ${{ steps.filter.outputs.frontend }}
      docs: ${{ steps.filter.outputs.docs }}
      frontend-tests: ${{ steps.filter.outputs.frontend-tests }}
      components-changes: ${{ steps.filter.outputs.components-changes }}
      starter-projects-changes: ${{ steps.filter.outputs.starter-projects-changes }}
      starter-projects: ${{ steps.filter.outputs.starter-projects }}
      components: ${{ steps.filter.outputs.components }}
      workspace: ${{ steps.filter.outputs.workspace }}
      api: ${{ steps.filter.outputs.api }}
      database: ${{ steps.filter.outputs.database }}
      docker: ${{ steps.filter.outputs.docker }}
      design-md: ${{ steps.filter.outputs.design-md }}
      docs-only: ${{
        steps.filter.outputs.docs == 'true' &&
        steps.filter.outputs.python != 'true' &&
        steps.filter.outputs.frontend != 'true' &&
        steps.filter.outputs['frontend-tests'] != 'true' &&
        steps.filter.outputs['components-changes'] != 'true' &&
        steps.filter.outputs['starter-projects-changes'] != 'true' &&
        steps.filter.outputs['starter-projects'] != 'true' &&
        steps.filter.outputs.components != 'true' &&
        steps.filter.outputs.workspace != 'true' &&
        steps.filter.outputs.api != 'true' &&
        steps.filter.outputs.database != 'true' &&
        steps.filter.outputs.docker != 'true'
        }}

    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}
      - name: Filter Paths
        id: filter
        uses: dorny/paths-filter@v3
        with:
          filters: ./.github/changes-filter.yaml
      - name: Set up Python
        uses: actions/setup-python@v6
        with:
          python-version: "3.12"
      - name: Install deps for coverage check
        run: |
          python -m pip install --upgrade pip
          pip install --disable-pip-version-check --no-cache-dir pyyaml
      - name: Validate Filter Coverage
        continue-on-error: true
        shell: bash
        run: |
          BASE_SHA="${{ github.event.pull_request.base.sha }}"
          BASE_REF="${{ github.event.pull_request.base.ref || 'main' }}"
          # Ensure base commit is present locally
          git fetch --no-tags --depth=1 origin "$BASE_REF" || true
          git fetch --no-tags --depth=1 origin "$BASE_SHA" || true
          git diff --name-only "${BASE_SHA:-origin/$BASE_REF}"...HEAD | python scripts/check_changes_filter.py

  test-backend:
    needs: [path-filter, set-ci-condition]
    name: Run Backend Tests
    if: |
      always() &&
      !cancelled() &&
      needs.set-ci-condition.outputs.should-run-tests == 'true' &&
      (
        inputs.run-all-tests ||
        (needs.path-filter.result != 'skipped' &&
        needs.path-filter.outputs.docs-only != 'true')
      )
    uses: ./.github/workflows/python_test.yml
    with:
      python-versions: ${{ inputs.python-versions || '["3.10", "3.14"]' }}
      runs-on: ${{ inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
      ref: ${{ inputs.ref || github.ref }}
    secrets:
      OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}"
      ANTHROPIC_API_KEY: "${{ secrets.ANTHROPIC_API_KEY }}"
      CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"

  lint-frontend:
    needs: [path-filter, set-ci-condition]
    name: Lint Frontend
    if: |
      always() &&
      !cancelled() &&
      needs.set-ci-condition.outputs.should-run-ci == 'true' &&
      (
        inputs.run-all-tests ||
        (needs.path-filter.result != 'skipped' &&
        needs.path-filter.outputs.frontend == 'true')
      )
    uses: ./.github/workflows/lint-js.yml

  test-frontend-unit:
    needs: [path-filter, set-ci-condition]
    name: Run Frontend Unit Tests
    if: |
      always() &&
      !cancelled() &&
      needs.set-ci-condition.outputs.should-run-tests == 'true' &&
      (
        inputs.run-all-tests ||
        (needs.path-filter.result != 'skipped' &&
        needs.path-filter.outputs.docs-only != 'true')
      )
    uses: ./.github/workflows/jest_test.yml
    with:
      ref: ${{ inputs.ref || github.ref }}
    secrets:
      CODECOV_TOKEN: "${{ secrets.CODECOV_TOKEN }}"

  test-frontend:
    needs: [path-filter, set-ci-condition]
    name: Run Frontend Tests
    if: |
      always() &&
      !cancelled() &&
      needs.set-ci-condition.outputs.should-run-tests == 'true' &&
      (
        inputs.run-all-tests ||
        (needs.path-filter.result != 'skipped' &&
        needs.path-filter.outputs.docs-only != 'true')
      )
    uses: ./.github/workflows/typescript_test.yml
    with:
      tests_folder: ${{ inputs.frontend-tests-folder }}
      release: ${{ inputs.release || false }}
      runs-on: ${{ inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
      ref: ${{ inputs.ref || github.ref }}
    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 }}"

  test-docs-build:
    needs: path-filter
    if: |
      always() &&
      !cancelled() &&
      (inputs.run-all-tests || needs.path-filter.outputs.docs == 'true')
    name: Test Docs Build
    uses: ./.github/workflows/docs_test.yml

  test-templates:
    needs: [path-filter, set-ci-condition]
    name: Test Starter Templates
    if: |
      always() &&
      !cancelled() &&
      needs.set-ci-condition.outputs.should-run-tests == 'true' &&
      (inputs.run-all-tests || needs.path-filter.outputs.python == 'true' || needs.path-filter.outputs.frontend == 'true')
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}

      - name: Set up Python 3.12
        uses: actions/setup-python@v6
        with:
          python-version: 3.12

      - name: Install uv
        uses: astral-sh/setup-uv@v6
        with:
          version: "latest"

      - name: Install dependencies
        run: |
          uv sync --dev

      - name: Test all starter project templates
        run: |
          uv run pytest src/backend/tests/unit/template/test_starter_projects.py -v -n auto

  test-docker:
    needs: [path-filter, set-ci-condition]
    name: Test Docker Images
    if: ${{ needs.path-filter.outputs.docker == 'true' && needs.set-ci-condition.outputs.should-run-tests == 'true' }}
    uses: ./.github/workflows/docker_test.yml
    secrets:
      DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
      DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}

  lint-design-md:
    needs: path-filter
    name: Lint DESIGN.md
    if: ${{ inputs.run-all-tests || needs.path-filter.outputs.design-md == 'true' }}
    runs-on: ubuntu-latest
    env:
      DESIGN_MD_CLI_VERSION: "0.1.1"
    steps:
      - name: Checkout code
        uses: actions/checkout@v6
        with:
          ref: ${{ inputs.ref || github.ref }}

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: "22"

      - name: Lint DESIGN.md
        run: npx --yes @google/design.md@${{ env.DESIGN_MD_CLI_VERSION }} lint DESIGN.md

  merge-frontend-coverage:
    name: Merge Frontend Jest + Playwright Coverage Reports
    needs: [path-filter, set-ci-condition, test-frontend-unit, test-frontend]
    if: |
      always() &&
      !cancelled() &&
      needs.set-ci-condition.outputs.should-run-tests == 'true' &&
      (
        inputs.run-all-tests ||
        (needs.path-filter.result != 'skipped' &&
        needs.path-filter.outputs.docs-only != 'true')
      )
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v6

      - name: Download Jest Coverage
        uses: actions/download-artifact@v4
        continue-on-error: true
        with:
          name: jest-coverage
          path: coverage/jest

      - name: Download Playwright Coverage
        uses: actions/download-artifact@v4
        continue-on-error: true
        with:
          pattern: playwright-coverage-*
          path: coverage/playwright
          merge-multiple: true

      - name: Merge Coverage
        run: |
          mkdir -p coverage/combined
          npx nyc merge coverage/playwright/individual-test coverage/combined/playwright.json || echo "Failed to merge Playwright coverage"
          npx nyc merge coverage/jest coverage/combined/jest.json || echo "Failed to merge Jest coverage"
          npx nyc merge coverage/combined coverage/combined/total-frontend-coverage.json || echo "Failed to combine Playwright + Jest coverage"

      - name: Generate Combined Report
        run: |
          npx nyc report \
            --reporter=html \
            --reporter=text \
            --temp-dir coverage/combined \
            --report-dir coverage/final

      - name: Upload Merged Frontend Coverage to Codecov
        if: env.CODECOV_TOKEN != ''
        env:
          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
        uses: codecov/codecov-action@v5
        with:
          token: ${{ env.CODECOV_TOKEN }}
          files: coverage/combined/total-frontend-coverage.json
          flags: frontend
          fail_ci_if_error: false

  # https://github.com/langchain-ai/langchain/blob/master/.github/workflows/check_diffs.yml
  ci_success:
    name: "CI Success"
    # Note: `lint-frontend` is intentionally NOT in this list — the Biome
    # check runs on every PR and shows up in the checks list, but its failure
    # does NOT block merge (informational only).
    needs:
      [
        validate-pr-title,
        test-backend,
        test-frontend-unit,
        test-frontend,
        test-docs-build,
        test-templates,
        test-docker,
        lint-design-md,
        set-ci-condition,
        path-filter,
        check-nightly-status,
        merge-frontend-coverage,
      ]

    if: ${{ always() }}
    runs-on: ubuntu-latest
    env:
      JOBS_JSON: ${{ toJSON(needs) }}
      RESULTS_JSON: ${{ toJSON(needs.*.result) }}
      # Skip nightly build check if only docs files changed
      DOCS_ONLY: ${{ needs.path-filter.outputs.docs-only }}
      EXIT_CODE: ${{ (contains(needs.*.result, 'failure') ||
        contains(needs.*.result, 'cancelled') ||
        (needs.check-nightly-status.outputs.should-proceed != 'true' && github.event_name != 'workflow_dispatch' && github.event_name != 'merge_group' && needs.path-filter.outputs.docs-only != 'true' && !contains(github.event.pull_request.labels.*.name, 'skip-nightly-check')))
        && '1' || '0' }}
    steps:
      - name: "CI Success"
        run: |
          echo "=== CI Status Summary ==="
          echo "Should run tests: ${{ needs.set-ci-condition.outputs.should-run-tests }}"
          echo "Should run CI: ${{ needs.set-ci-condition.outputs.should-run-ci }}"
          echo "Nightly build status: ${{ needs.check-nightly-status.outputs.should-proceed }}"
          echo "Event type: ${{ github.event_name }}"
          echo "Docs only changes: $DOCS_ONLY"
          echo "Python changes: ${{ needs.path-filter.outputs.python }}"
          echo "Frontend changes: ${{ needs.path-filter.outputs.frontend }}"
          echo "Docs changes: ${{ needs.path-filter.outputs.docs }}"
          echo ""

          # Check for job failures
          if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then
            echo "❌ CI FAILED: One or more jobs failed"
            echo ""
            echo "Failed jobs:"

            # Dynamically list failed jobs with helpful descriptions
            echo "$JOBS_JSON" | jq -r '
              to_entries[]
              | select(.value.result=="failure")
              | .key as $job
              | if $job == "validate-pr-title" then
                  "  - PR Title Validation: Title ends with ellipsis (...). Use a complete, descriptive title."
                elif $job == "test-backend" then
                  "  - Backend Tests: Check Python code, tests, and dependencies"
                elif $job == "test-frontend-unit" then
                  "  - Frontend Unit Tests: Check React components and unit test logic"
                elif $job == "test-frontend" then
                  "  - Frontend E2E Tests: Check integration tests and UI functionality"
                elif $job == "test-docs-build" then
                  "  - Documentation Build: Check documentation syntax and build process"
                elif $job == "test-templates" then
                  "  - Template Tests: Check starter project templates"
                elif $job == "test-docker" then
                  "  - Docker Tests: Check Docker image builds and version verification"
                elif $job == "lint-design-md" then
                  "  - DESIGN.md Lint: Check token references, WCAG contrast, and spec compliance"
                elif $job == "path-filter" then
                  "  - Path Filter: File path filtering failed"
                elif $job == "set-ci-condition" then
                  "  - CI Condition Check: CI condition evaluation failed"
                elif $job == "check-nightly-status" then
                  "  - Nightly Status Check: PyPI package status check failed"
                else
                  "  - \($job): See job log for details"
                end
            '

            echo ""
            echo "🔧 Next steps:"
            echo "  1. Review the failed job logs above"
            echo "  2. Fix the identified issues in your code"
            echo "  3. Push your changes to re-run the tests"

          elif [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then
            echo "⚠️  CI CANCELLED: One or more jobs were cancelled"
            echo ""
            echo "🔧 Next steps:"
            echo "  1. Check if the cancellation was intentional"
            echo "  2. Re-run the workflow if needed"

          elif [[ "${{ needs.check-nightly-status.outputs.should-proceed }}" != "true" && "${{ github.event_name }}" != "workflow_dispatch" && "$DOCS_ONLY" != "true" ]]; then
            echo "🚫 CI BLOCKED: Nightly build is broken"
            echo ""
            echo "The nightly PyPI package was not updated today, indicating the nightly build failed."
            echo ""
            echo "🔧 Next steps:"
            echo "  1. Work with the team to investigate and fix the nightly build"
            echo "  2. Check the nightly build logs for errors"
            echo "  3. Once the nightly build is fixed and publishes successfully, re-run this workflow"
            echo "  4. Alternatively, use 'workflow_dispatch' to manually override this check if needed"
            echo "  5. Note: PRs with only documentation changes can bypass this check"

          else
            echo "✅ CI SUCCESS: All checks passed!"
            echo ""
            echo "🎉 Your changes are ready:"
            echo "  - All tests passed"
            echo "  - Code quality checks passed"
            echo "  - Nightly build is healthy"
          fi

          echo ""
          echo "Exit code: $EXIT_CODE"
          exit $EXIT_CODE
