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]==, 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/lfx && uv lock && cd ../.. git add pyproject.toml src/backend/base/pyproject.toml src/lfx/pyproject.toml src/sdk/pyproject.toml uv.lock # The nightly keeps canonical package names and the stable `lfx-*` bundles # are not modified (see src/bundles/NIGHTLY.md), so no bundle pyprojects ride this commit. git commit -m "Update version for nightly" 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 }} stress-tests: if: github.repository == 'langflow-ai/langflow' && !inputs.skip_backend_tests name: Run Stress Tests needs: [resolve-release-branch, create-nightly-tag] uses: ./.github/workflows/stress-tests.yml with: ref: ${{ needs.resolve-release-branch.outputs.branch }} 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, stress-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, stress-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.stress-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.stress-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.stress-tests.result }}" == "failure" ]; then FAILED_JOB="stress-tests (non-blocking)" 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\": \"\" } ] } ] }" ${{ 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": "" } ] } ] }' ${{ secrets.LANGFLOW_ENG_SLACK_WEBHOOK_URL }}