langflow-ai/langflow
35 workflows · maturity 83% · 14 patterns · GitHub ↗
Practices
✓ Matrix✓ Permissions✓ Security scan○ AI review✓ Cache✓ Concurrency✓ Reusable workflows
Detected patterns
Security dimensions
Tools: github/codeql-action/analyze, github/codeql-action/autobuild, github/codeql-action/init
Workflows (35)
add-labels .github/workflows/add-labels.yml
View raw YAML
name: Manage Review Labels
on:
pull_request_review:
types: [submitted]
jobs:
label-on-review:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Manage LGTM Review Label
uses: actions/github-script@v8
with:
script: |
const LGTM_LABEL = 'lgtm';
// Extract review details
const { state: reviewState } = context.payload.review;
const pullRequestNumber = context.payload.pull_request.number;
const repoDetails = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequestNumber
};
// Log review information
console.log(`Processing review for PR #${pullRequestNumber}`);
console.log(`Review state: ${reviewState}`);
// Helper function to check for LGTM label
async function hasLgtmLabel() {
const { data: labels } = await github.rest.issues.listLabelsOnIssue(repoDetails);
return labels.some(label => label.name === LGTM_LABEL);
}
if (reviewState === 'approved') {
const lgtmExists = await hasLgtmLabel();
if (!lgtmExists) {
console.log(`Adding ${LGTM_LABEL} label to PR #${pullRequestNumber}`);
await github.rest.issues.addLabels({
...repoDetails,
labels: [LGTM_LABEL]
});
console.log('Label added successfully');
} else {
console.log(`${LGTM_LABEL} label already exists`);
}
} else if (reviewState === 'changes_requested') {
const lgtmExists = await hasLgtmLabel();
if (lgtmExists) {
console.log(`Removing ${LGTM_LABEL} label from PR #${pullRequestNumber}`);
await github.rest.issues.removeLabel({
...repoDetails,
name: LGTM_LABEL
});
console.log('Label removed successfully');
} else {
console.log(`No ${LGTM_LABEL} label to remove`);
}
}
auto-update .github/workflows/auto-update.yml
View raw YAML
name: Auto-update
on:
push:
branches:
- main
jobs:
Auto:
name: Auto-update
runs-on: ubuntu-latest
steps:
- uses: tibdex/auto-update@v2
ci .github/workflows/ci.yml
View raw YAML
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']"
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']"
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 }}"
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 }}
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"]' }}
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 }}"
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 }}"
lint-backend:
needs: path-filter
if: |
always() &&
!cancelled() &&
(inputs.run-all-tests || needs.path-filter.outputs.python == 'true')
name: Lint Backend
uses: ./.github/workflows/lint-py.yml
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 }}
# https://github.com/langchain-ai/langchain/blob/master/.github/workflows/check_diffs.yml
ci_success:
name: "CI Success"
needs:
[
test-backend,
test-frontend-unit,
test-frontend,
lint-backend,
test-docs-build,
test-templates,
test-docker,
set-ci-condition,
path-filter,
check-nightly-status,
]
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' && needs.path-filter.outputs.docs-only != 'true'))
&& '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 == "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 == "lint-backend" then
" - Backend Linting: Run '\''make format_backend'\'' then '\''make lint'\'' to fix code style issues"
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 == "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
codeql matrix security .github/workflows/codeql.yml
View raw YAML
name: "CodeQL"
on:
push:
branches: [ 'dev', 'main' ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ 'dev' ]
schedule:
- cron: '17 2 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python', 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Use only 'java' to analyze code written in Java, Kotlin or both
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v3
# ℹ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{matrix.language}}"
community-label .github/workflows/community-label.yml
View raw YAML
name: Add Community Label
on:
pull_request_target:
types: [opened]
jobs:
add-label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- name: Add community label
if: github.event.pull_request.author_association != 'MEMBER' && github.event.pull_request.author_association != 'OWNER' && github.event.pull_request.author_association != 'COLLABORATOR'
uses: actions/github-script@v8
with:
script: |
const pullRequestNumber = context.payload.pull_request.number;
const repoDetails = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: pullRequestNumber
};
await github.rest.issues.addLabels({
...repoDetails,
labels: ['community']
});
conventional-labels .github/workflows/conventional-labels.yml
View raw YAML
# Warning, do not check out untrusted code with
# the pull_request_target event.
name: Label PRs with Conventional Commits
on:
pull_request_target:
types: [opened, edited, synchronize]
merge_group:
jobs:
validate-pr:
name: Validate PR
runs-on: ubuntu-latest
steps:
- name: Validate the pull request
id: validate
uses: Namchee/conventional-pr@v0.15.6
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
issue: false
label:
needs: validate-pr
name: Label PR
runs-on: ubuntu-latest
if: ${{ github.event.pull_request.user.type != 'Bot'}}
steps:
- uses: bcoe/conventional-release-labels@v1
with:
type_labels: '{"feat": "enhancement","fix": "bug","docs": "documentation","style": "style","refactor": "refactor","perf": "performance","test": "test","chore": "chore","build": "build"}'
create-release .github/workflows/create-release.yml
View raw YAML
name: Create Release
on:
workflow_dispatch:
inputs:
version:
description: "Version to release"
required: true
type: string
ref:
description: "Commit to tag the release"
required: true
type: string
pre_release:
description: "Pre-release tag"
required: true
type: boolean
jobs:
create_release:
name: Create Release Job
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v7
with:
name: dist-main
path: dist
- name: Create Release Notes
uses: ncipollo/release-action@v1
with:
artifacts: "dist/*"
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
generateReleaseNotes: true
prerelease: ${{ inputs.pre_release }}
tag: v${{ inputs.version }}
commit: ${{ inputs.ref }}cross-platform-test matrix .github/workflows/cross-platform-test.yml
View raw YAML
name: Cross-Platform Installation Test
on:
workflow_dispatch:
inputs:
langflow-version:
description: "Langflow version to test from PyPI (leave empty to test from source)"
required: false
type: string
default: ""
workflow_call:
inputs:
base-artifact-name:
description: "Name of the base package artifact"
required: true
type: string
main-artifact-name:
description: "Name of the main package artifact"
required: true
type: string
lfx-artifact-name:
description: "Name of the LFX package artifact"
required: false
type: string
pre_release:
description: "Whether this is a pre-release build"
required: false
type: boolean
default: false
jobs:
build-if-needed:
name: Build Packages (if no artifacts provided)
runs-on: ubuntu-latest
if: inputs.langflow-version == '' && contains(github.workflow_ref, 'cross-platform-test.yml')
outputs:
base-artifact-name: ${{ steps.set-names.outputs.base-artifact-name }}
main-artifact-name: ${{ steps.set-names.outputs.main-artifact-name }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
- name: Install the project
run: uv sync
- name: Install frontend dependencies
run: make install_frontendci
- name: Build frontend
run: make build_frontend
- name: Build base package
run: make build_langflow_base args="--wheel"
- name: Move base package to correct directory
run: |
# Base package builds to dist/ but should be in src/backend/base/dist/
mkdir -p src/backend/base/dist
mv dist/langflow_base*.whl src/backend/base/dist/
- name: Build main package
run: make build_langflow args="--wheel"
- name: Upload base artifact
uses: actions/upload-artifact@v6
with:
name: adhoc-dist-base
path: src/backend/base/dist
- name: Upload main artifact
uses: actions/upload-artifact@v6
with:
name: adhoc-dist-main
path: dist
- name: Set artifact names
id: set-names
run: |
echo "base-artifact-name=adhoc-dist-base" >> $GITHUB_OUTPUT
echo "main-artifact-name=adhoc-dist-main" >> $GITHUB_OUTPUT
test-installation-stable:
name: Install & Run - ${{ matrix.os }} ${{ matrix.arch }} ${{ matrix.python-version }}
needs: [build-if-needed]
if: always() && (needs.build-if-needed.result == 'success' || needs.build-if-needed.result == 'skipped')
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
# Linux AMD64
- os: linux
arch: amd64
runner: ubuntu-latest
python-version: "3.10"
- os: linux
arch: amd64
runner: ubuntu-latest
python-version: "3.12"
# macOS AMD64
- os: macos
arch: amd64
runner: macos-latest-large
python-version: "3.10"
- os: macos
arch: amd64
runner: macos-latest-large
python-version: "3.12"
# macOS ARM64 (Apple Silicon)
- os: macos
arch: arm64
runner: macos-latest
python-version: "3.10"
- os: macos
arch: arm64
runner: macos-latest
python-version: "3.12"
# Windows AMD64
- os: windows
arch: amd64
runner: windows-latest
python-version: "3.10"
- os: windows
arch: amd64
runner: windows-latest
python-version: "3.12"
steps:
- name: Determine install method
id: install-method
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ -n "${{ inputs.langflow-version }}" ]; then
echo "method=pypi" >> $GITHUB_OUTPUT
else
echo "method=wheel" >> $GITHUB_OUTPUT
fi
else
# workflow_call always uses wheel method (backward compatibility)
echo "method=wheel" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.arch == 'amd64' && 'x64' || matrix.arch }}
- name: Setup UV
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
ignore-empty-workdir: true
- name: Install Protocol Buffers (macOS AMD64)
if: matrix.os == 'macos' && matrix.arch == 'amd64'
run: |
if ! command -v protoc &> /dev/null; then
brew install protobuf
else
echo "protoc already installed, skipping"
fi
shell: bash
# Download artifacts for wheel installation
- name: Download LFX package artifact
if: steps.install-method.outputs.method == 'wheel' && inputs.lfx-artifact-name != ''
uses: actions/download-artifact@v7
with:
name: ${{ inputs.lfx-artifact-name }}
path: ./lfx-dist
- name: Download base package artifact
if: steps.install-method.outputs.method == 'wheel'
uses: actions/download-artifact@v7
with:
name: ${{ inputs.base-artifact-name || needs.build-if-needed.outputs.base-artifact-name || 'adhoc-dist-base' }}
path: ./base-dist
- name: Download main package artifact
if: steps.install-method.outputs.method == 'wheel'
uses: actions/download-artifact@v7
with:
name: ${{ inputs.main-artifact-name || needs.build-if-needed.outputs.main-artifact-name || 'adhoc-dist-main' }}
path: ./main-dist
- name: Create fresh virtual environment
run: |
uv venv test-env --seed
shell: bash
# Wheel installation steps
- name: Install LFX package from wheel (Windows)
if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && inputs.lfx-artifact-name != ''
run: |
ls -la ./lfx-dist/
find ./lfx-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./lfx-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "$WHEEL_FILE"
else
echo "No wheel file found in ./lfx-dist/"
exit 1
fi
shell: bash
- name: Install base package from wheel (Windows)
if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows'
run: |
ls -la ./base-dist/
find ./base-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./base-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "$WHEEL_FILE"
else
echo "No wheel file found in ./base-dist/"
exit 1
fi
shell: bash
- name: Install main package from wheel (Windows)
if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows'
run: |
ls -la ./main-dist/
find ./main-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./main-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "$WHEEL_FILE"
else
echo "No wheel file found in ./main-dist/"
exit 1
fi
shell: bash
- name: Install LFX package from wheel (Unix)
if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && inputs.lfx-artifact-name != ''
run: |
ls -la ./lfx-dist/
find ./lfx-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./lfx-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/bin/python "$WHEEL_FILE"
else
echo "No wheel file found in ./lfx-dist/"
exit 1
fi
shell: bash
- name: Install base package from wheel (Unix)
if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows'
run: |
ls -la ./base-dist/
find ./base-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./base-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/bin/python "$WHEEL_FILE"
else
echo "No wheel file found in ./base-dist/"
exit 1
fi
shell: bash
- name: Install main package from wheel (Unix)
if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows'
run: |
ls -la ./main-dist/
find ./main-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./main-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/bin/python "$WHEEL_FILE"
else
echo "No wheel file found in ./main-dist/"
exit 1
fi
shell: bash
# PyPI installation steps
- name: Install langflow from PyPI (Windows)
if: steps.install-method.outputs.method == 'pypi' && matrix.os == 'windows'
run: |
if [ -n "${{ inputs.langflow-version }}" ]; then
uv pip install --python ./test-env/Scripts/python.exe langflow==${{ inputs.langflow-version }}
else
uv pip install --python ./test-env/Scripts/python.exe langflow
fi
shell: bash
- name: Install langflow from PyPI (Unix)
if: steps.install-method.outputs.method == 'pypi' && matrix.os != 'windows'
run: |
if [ -n "${{ inputs.langflow-version }}" ]; then
uv pip install --python ./test-env/bin/python langflow==${{ inputs.langflow-version }}
else
uv pip install --python ./test-env/bin/python langflow
fi
shell: bash
# Install additional dependencies
- name: Install additional dependencies (Windows)
if: matrix.os == 'windows'
run: |
uv pip install --python ./test-env/Scripts/python.exe openai
shell: bash
- name: Install additional dependencies (Unix)
if: matrix.os != 'windows'
run: |
uv pip install --python ./test-env/bin/python openai
shell: bash
# Test steps
- name: Test CLI help command
if: matrix.os == 'windows'
run: |
uv run --python ./test-env/Scripts/python.exe python -m langflow --help
shell: cmd
- name: Test CLI help command (Unix)
if: matrix.os != 'windows'
run: |
uv run --python ./test-env/bin/python python -m langflow --help
shell: bash
- name: Test server startup (Windows)
if: matrix.os == 'windows'
timeout-minutes: 5
run: |
# Start server in background
$serverProcess = Start-Process -FilePath ".\test-env\Scripts\python.exe" -ArgumentList "-m", "langflow", "run", "--host", "localhost", "--port", "7860", "--backend-only" -PassThru -WindowStyle Hidden
# Wait for server to be ready (GitHub Actions will timeout after 5 minutes)
do {
try {
$response = Invoke-WebRequest -Uri "http://localhost:7860/health_check" -UseBasicParsing -TimeoutSec 5
if ($response.StatusCode -eq 200) {
Write-Host "✅ Server is ready on ${{ matrix.os }}-${{ matrix.arch }}"
break
}
} catch {
Start-Sleep -Seconds 5
}
} while ($true)
# Stop the server process
Stop-Process -Id $serverProcess.Id -Force -ErrorAction SilentlyContinue
shell: powershell
- name: Test server startup (Unix)
if: matrix.os != 'windows'
timeout-minutes: 5
env:
OBJC_DISABLE_INITIALIZE_FORK_SAFETY: YES
run: |
# Start server in background
./test-env/bin/python -m langflow run --host localhost --port 7860 --backend-only &
SERVER_PID=$!
# Wait for server to be ready (GitHub Actions will timeout after 5 minutes)
while true; do
if curl -f http://localhost:7860/health_check >/dev/null 2>&1; then
echo "✅ Server is ready on ${{ matrix.os }}-${{ matrix.arch }}"
break
fi
sleep 5
done
# Clean shutdown
kill $SERVER_PID 2>/dev/null || true
sleep 5
shell: bash
- name: Test import in Python (Windows)
if: matrix.os == 'windows'
run: |
test-env\Scripts\python.exe -c "
try:
import langflow
print('✅ langflow import successful on ${{ matrix.os }}-${{ matrix.arch }}')
except Exception as e:
print(f'❌ langflow import failed on ${{ matrix.os }}-${{ matrix.arch }}: {e}')
exit(1)
"
shell: cmd
- name: Test import in Python (Unix)
if: matrix.os != 'windows'
run: |
./test-env/bin/python -c "
try:
import langflow
print('✅ langflow import successful on ${{ matrix.os }}-${{ matrix.arch }}')
except Exception as e:
print(f'❌ langflow import failed on ${{ matrix.os }}-${{ matrix.arch }}: {e}')
exit(1)
"
shell: bash
test-installation-experimental:
name: Install & Run - ${{ matrix.os }} ${{ matrix.arch }} ${{ matrix.python-version }} (Experimental)
needs: [build-if-needed]
if: always() && (needs.build-if-needed.result == 'success' || needs.build-if-needed.result == 'skipped')
runs-on: ${{ matrix.runner }}
continue-on-error: true
strategy:
fail-fast: false
matrix:
include:
# Python 3.13 - Experimental
- os: linux
arch: amd64
runner: ubuntu-latest
python-version: "3.13"
- os: windows
arch: amd64
runner: windows-latest
python-version: "3.13"
- os: macos
arch: amd64
runner: macos-latest-large
python-version: "3.13"
- os: macos
arch: arm64
runner: macos-latest
python-version: "3.13"
steps:
- name: Determine install method
id: install-method
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ -n "${{ inputs.langflow-version }}" ]; then
echo "method=pypi" >> $GITHUB_OUTPUT
else
echo "method=wheel" >> $GITHUB_OUTPUT
fi
else
# workflow_call always uses wheel method (backward compatibility)
echo "method=wheel" >> $GITHUB_OUTPUT
fi
shell: bash
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
architecture: ${{ matrix.arch == 'amd64' && 'x64' || matrix.arch }}
- name: Setup UV
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
ignore-empty-workdir: true
- name: Install Protocol Buffers (macOS AMD64)
if: matrix.os == 'macos' && matrix.arch == 'amd64'
run: |
if ! command -v protoc &> /dev/null; then
brew install protobuf
else
echo "protoc already installed, skipping"
fi
shell: bash
# Download artifacts for wheel installation
- name: Download LFX package artifact
if: steps.install-method.outputs.method == 'wheel' && inputs.lfx-artifact-name != ''
uses: actions/download-artifact@v7
with:
name: ${{ inputs.lfx-artifact-name }}
path: ./lfx-dist
- name: Download base package artifact
if: steps.install-method.outputs.method == 'wheel'
uses: actions/download-artifact@v7
with:
name: ${{ inputs.base-artifact-name || needs.build-if-needed.outputs.base-artifact-name || 'adhoc-dist-base' }}
path: ./base-dist
- name: Download main package artifact
if: steps.install-method.outputs.method == 'wheel'
uses: actions/download-artifact@v7
with:
name: ${{ inputs.main-artifact-name || needs.build-if-needed.outputs.main-artifact-name || 'adhoc-dist-main' }}
path: ./main-dist
- name: Create fresh virtual environment
run: |
uv venv test-env --seed
shell: bash
# Wheel installation steps
- name: Install LFX package from wheel (Windows)
if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows' && inputs.lfx-artifact-name != ''
run: |
ls -la ./lfx-dist/
find ./lfx-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./lfx-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "$WHEEL_FILE"
else
echo "No wheel file found in ./lfx-dist/"
exit 1
fi
shell: bash
- name: Install base package from wheel (Windows)
if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows'
run: |
ls -la ./base-dist/
find ./base-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./base-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "$WHEEL_FILE"
else
echo "No wheel file found in ./base-dist/"
exit 1
fi
shell: bash
- name: Install main package from wheel (Windows)
if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows'
run: |
ls -la ./main-dist/
find ./main-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./main-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/Scripts/python.exe "$WHEEL_FILE"
else
echo "No wheel file found in ./main-dist/"
exit 1
fi
shell: bash
- name: Install LFX package from wheel (Unix)
if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows' && inputs.lfx-artifact-name != ''
run: |
ls -la ./lfx-dist/
find ./lfx-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./lfx-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/bin/python "$WHEEL_FILE"
else
echo "No wheel file found in ./lfx-dist/"
exit 1
fi
shell: bash
- name: Install base package from wheel (Unix)
if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows'
run: |
ls -la ./base-dist/
find ./base-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./base-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/bin/python "$WHEEL_FILE"
else
echo "No wheel file found in ./base-dist/"
exit 1
fi
shell: bash
- name: Install main package from wheel (Unix)
if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows'
run: |
ls -la ./main-dist/
find ./main-dist -name "*.whl" -type f
WHEEL_FILE=$(find ./main-dist -name "*.whl" -type f | head -1)
if [ -n "$WHEEL_FILE" ]; then
uv pip install --prerelease=allow --python ./test-env/bin/python "$WHEEL_FILE"
else
echo "No wheel file found in ./main-dist/"
exit 1
fi
shell: bash
- name: Force reinstall local wheels to prevent downgrades (Windows)
if: steps.install-method.outputs.method == 'wheel' && matrix.os == 'windows'
run: |
# Reinstall LFX if it was provided
if [ -n "${{ inputs.lfx-artifact-name }}" ]; then
LFX_WHEEL=$(find ./lfx-dist -name "*.whl" -type f | head -1)
if [ -n "$LFX_WHEEL" ]; then
echo "Force reinstalling LFX: $LFX_WHEEL"
NO_DEPS="--no-deps"
if [ "${{ inputs.pre_release }}" = "true" ]; then
NO_DEPS=""
fi
uv pip install --force-reinstall $NO_DEPS --prerelease=allow --python ./test-env/Scripts/python.exe "$LFX_WHEEL"
fi
fi
# Reinstall base
BASE_WHEEL=$(find ./base-dist -name "*.whl" -type f | head -1)
if [ -n "$BASE_WHEEL" ]; then
echo "Force reinstalling langflow-base: $BASE_WHEEL"
NO_DEPS="--no-deps"
if [ "${{ inputs.pre_release }}" = "true" ]; then
NO_DEPS=""
fi
uv pip install --force-reinstall $NO_DEPS --prerelease=allow --python ./test-env/Scripts/python.exe "$BASE_WHEEL"
fi
shell: bash
- name: Force reinstall local wheels to prevent downgrades (Unix)
if: steps.install-method.outputs.method == 'wheel' && matrix.os != 'windows'
run: |
# Reinstall LFX if it was provided
if [ -n "${{ inputs.lfx-artifact-name }}" ]; then
LFX_WHEEL=$(find ./lfx-dist -name "*.whl" -type f | head -1)
if [ -n "$LFX_WHEEL" ]; then
echo "Force reinstalling LFX: $LFX_WHEEL"
NO_DEPS="--no-deps"
if [ "${{ inputs.pre_release }}" = "true" ]; then
NO_DEPS=""
fi
uv pip install --force-reinstall $NO_DEPS --prerelease=allow --python ./test-env/bin/python "$LFX_WHEEL"
fi
fi
# Reinstall base
BASE_WHEEL=$(find ./base-dist -name "*.whl" -type f | head -1)
if [ -n "$BASE_WHEEL" ]; then
echo "Force reinstalling langflow-base: $BASE_WHEEL"
NO_DEPS="--no-deps"
if [ "${{ inputs.pre_release }}" = "true" ]; then
NO_DEPS=""
fi
uv pip install --force-reinstall $NO_DEPS --prerelease=allow --python ./test-env/bin/python "$BASE_WHEEL"
fi
shell: bash
# PyPI installation steps
- name: Install langflow from PyPI (Windows)
if: steps.install-method.outputs.method == 'pypi' && matrix.os == 'windows'
run: |
if [ -n "${{ inputs.langflow-version }}" ]; then
uv pip install --python ./test-env/Scripts/python.exe langflow==${{ inputs.langflow-version }}
else
uv pip install --python ./test-env/Scripts/python.exe langflow
fi
shell: bash
- name: Install langflow from PyPI (Unix)
if: steps.install-method.outputs.method == 'pypi' && matrix.os != 'windows'
run: |
if [ -n "${{ inputs.langflow-version }}" ]; then
uv pip install --python ./test-env/bin/python langflow==${{ inputs.langflow-version }}
else
uv pip install --python ./test-env/bin/python langflow
fi
shell: bash
# Install additional dependencies
- name: Install additional dependencies (Windows)
if: matrix.os == 'windows'
run: |
uv pip install --python ./test-env/Scripts/python.exe openai
shell: bash
- name: Install additional dependencies (Unix)
if: matrix.os != 'windows'
run: |
uv pip install --python ./test-env/bin/python openai
shell: bash
# Test steps
- name: Test CLI help command (Windows)
if: matrix.os == 'windows'
run: |
uv run --python ./test-env/Scripts/python.exe python -m langflow --help
shell: cmd
- name: Test CLI help command (Unix)
if: matrix.os != 'windows'
run: |
uv run --python ./test-env/bin/python python -m langflow --help
shell: bash
- name: Test server startup (Windows)
if: matrix.os == 'windows'
timeout-minutes: 5
run: |
# Start server in background
$serverProcess = Start-Process -FilePath ".\test-env\Scripts\python.exe" -ArgumentList "-m", "langflow", "run", "--host", "localhost", "--port", "7860", "--backend-only" -PassThru -WindowStyle Hidden
# Wait for server to be ready (GitHub Actions will timeout after 5 minutes)
do {
try {
$response = Invoke-WebRequest -Uri "http://localhost:7860/health_check" -UseBasicParsing -TimeoutSec 5
if ($response.StatusCode -eq 200) {
Write-Host "✅ Server is ready on ${{ matrix.os }}-${{ matrix.arch }}"
break
}
} catch {
Start-Sleep -Seconds 5
}
} while ($true)
# Stop the server process
Stop-Process -Id $serverProcess.Id -Force -ErrorAction SilentlyContinue
shell: powershell
- name: Test server startup (Unix)
if: matrix.os != 'windows'
timeout-minutes: 5
env:
OBJC_DISABLE_INITIALIZE_FORK_SAFETY: YES
run: |
# Start server in background
./test-env/bin/python -m langflow run --host localhost --port 7860 --backend-only &
SERVER_PID=$!
# Wait for server to be ready (GitHub Actions will timeout after 5 minutes)
while true; do
if curl -f http://localhost:7860/health_check >/dev/null 2>&1; then
echo "✅ Server is ready on ${{ matrix.os }}-${{ matrix.arch }}"
break
fi
sleep 5
done
# Clean shutdown
kill $SERVER_PID 2>/dev/null || true
sleep 5
shell: bash
- name: Test import in Python (Windows)
if: matrix.os == 'windows'
run: |
test-env\Scripts\python.exe -c "
try:
import langflow
print('✅ langflow import successful on ${{ matrix.os }}-${{ matrix.arch }}')
except Exception as e:
print(f'❌ langflow import failed on ${{ matrix.os }}-${{ matrix.arch }}: {e}')
exit(1)
"
shell: cmd
- name: Test import in Python (Unix)
if: matrix.os != 'windows'
run: |
./test-env/bin/python -c "
try:
import langflow
print('✅ langflow import successful on ${{ matrix.os }}-${{ matrix.arch }}')
except Exception as e:
print(f'❌ langflow import failed on ${{ matrix.os }}-${{ matrix.arch }}: {e}')
exit(1)
"
shell: bash
test-summary:
name: Cross-Platform Test Summary
needs: [test-installation-stable, test-installation-experimental]
runs-on: ubuntu-latest
if: always()
steps:
- name: Check test results
run: |
stable_result="${{ needs.test-installation-stable.result }}"
experimental_result="${{ needs.test-installation-experimental.result }}"
echo "Stable platforms result: $stable_result"
echo "Experimental platforms result: $experimental_result"
# Stable platforms must succeed, experimental can fail
if [ "$stable_result" = "success" ]; then
if [ "$experimental_result" = "success" ]; then
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ -n "${{ inputs.langflow-version }}" ]; then
echo "✅ All PyPI installation tests passed (including Python 3.13)"
else
echo "✅ All source build and installation tests passed (including Python 3.13)"
fi
else
echo "✅ All cross-platform tests passed - PyPI upload can proceed"
fi
else
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ -n "${{ inputs.langflow-version }}" ]; then
echo "✅ PyPI installation tests passed (Python 3.13 experimental failures are acceptable)"
else
echo "✅ Source build and installation tests passed (Python 3.13 experimental failures are acceptable)"
fi
else
echo "✅ Cross-platform tests passed - Python 3.13 experimental failures are acceptable - PyPI upload can proceed"
fi
fi
elif [ "$stable_result" = "failure" ]; then
echo "❌ Critical platform tests failed - blocking release"
echo "Stable platforms (Python 3.10, 3.12) must pass for release"
exit 1
elif [ "$stable_result" = "cancelled" ]; then
echo "❌ Critical platform tests were cancelled"
exit 1
else
echo "❌ Critical platform tests were skipped unexpectedly"
exit 1
fi
deploy-docs-draft .github/workflows/deploy-docs-draft.yml
View raw YAML
name: Pull Request Docs Draft
on:
pull_request:
branches:
- '**'
paths:
- 'docs/**'
- '.github/workflows/deploy-docs-draft.yml'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
jobs:
build-and-deploy:
runs-on: ubuntu-latest
if: "! github.event.pull_request.head.repo.fork"
steps:
- name: Checkout Repository
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: ./docs/package-lock.json
- name: Validate Branch Names
run: |
# Check if branch names contain invalid characters. Only alphanumeric, _, -, ., and / are allowed.
validate_branch_name() {
local branch_name="$1"
if [[ ! "$branch_name" =~ ^[a-zA-Z0-9/_\.-]+$ ]]; then
echo "Error: Branch name contains invalid characters. Only alphanumeric, _, -, ., and / are allowed."
exit 1
fi
}
validate_branch_name "${{ github.event.pull_request.head.ref }}"
- name: Extract Branch Names
id: extract_branch
run: |
# Extract and transform branch names
extract_branch() {
local input_branch="$1"
# Check if input_branch starts with "refs/heads/"
if [[ "$input_branch" == refs/heads/* ]]; then
# Remove "refs/heads/" prefix safely using parameter expansion
branch_name="${input_branch#refs/heads/}"
echo "$branch_name"
else
echo "$input_branch"
fi
}
# Transform branch names in form of `refs/heads/main` to `main`
draft_branch=$(extract_branch "${{ github.event.pull_request.head.ref }}")
# Replace / with - in the draft branch name to use as a directory name
draft_directory=$(echo "$draft_branch" | tr / -)
# Safe echo to $GITHUB_OUTPUT
{
echo "draft_branch=$draft_branch"
echo "draft_directory=$draft_directory"
} >> "$GITHUB_OUTPUT"
- name: Set Draft URL
id: draft_url
if: success()
run: |
echo "url=${{ vars.DOCS_DRAFT_BASE_URL }}/langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}/index.html" >> $GITHUB_OUTPUT
- name: Install dependencies
run: cd docs && npm install
- name: Build website
if: success()
run: |
set -o pipefail
cd docs
npm run build |& tee $GITHUB_WORKSPACE/build.log
env:
BASE_URL: /langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}
FORCE_COLOR: 0 # Disable color output
SEGMENT_PUBLIC_WRITE_KEY: ${{ vars.DOCS_DRAFT_IBM_SEGMENT_PUBLIC_WRITE_KEY }}
- name: Check Build Result
id: buildLogFail
if: failure()
run: |
MULTILINE_LOG=$(cat $GITHUB_WORKSPACE/build.log)
echo "BUILD_FAILURE<<EOF" >> $GITHUB_ENV
echo $MULTILINE_LOG >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Hide Previous Build Comments
if: ${{ github.event.pull_request.number && (success() || failure()) }}
run: |
set -e
# Get all comments on the PR that match our build comments
comments=$(gh api repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \
--jq '.[] | select(.body | test("Build failure! :x:|Build successful! :white_check_mark:")) | .node_id')
# Minimize each matching comment using GraphQL API
if [[ -n "$comments" ]]; then
echo "Found previous build comments to hide"
while IFS= read -r comment_id; do
if [[ -n "$comment_id" ]]; then
echo "Minimizing comment: $comment_id"
gh api graphql \
--field id="$comment_id" \
--field classifier="OUTDATED" \
--raw-field query='
mutation($id: ID!, $classifier: ReportedContentClassifiers!) {
minimizeComment(input: { subjectId: $id, classifier: $classifier }) {
minimizedComment {
isMinimized
}
}
}' || echo "Failed to minimize comment $comment_id, continuing..."
echo
fi
done <<< "$comments"
else
echo "No previous build comments found to hide"
fi
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Assemble Build Success Comment
if: success()
run: |
build_success_comment="Build successful! :white_check_mark:"
build_success_comment+="\nDeploying docs draft."
echo "BUILD_SUCCESS_COMMENT<<EOF" >> $GITHUB_ENV
echo -e "$build_success_comment" >> $GITHUB_ENV
echo "EOF" >> $GITHUB_ENV
- name: Create Build Success Comment
if: success()
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body: "${{ env.BUILD_SUCCESS_COMMENT }}"
reactions: rocket
- name: Create Build Failure Comment
if: failure()
uses: peter-evans/create-or-update-comment@v4
with:
issue-number: ${{ github.event.pull_request.number }}
body: |
Build failure! :x:
> ${{ env.BUILD_FAILURE }}
reactions: confused
- name: Find Comment
id: fc
if: success()
uses: peter-evans/find-comment@v3
with:
issue-number: ${{ github.event.pull_request.number }}
body-includes: Build successful!
direction: last
- name: Configure AWS CLI
if: success()
run: |
aws configure set aws_access_key_id ${{ secrets.DOCS_AWS_ACCESS_KEY_ID }}
aws configure set aws_secret_access_key ${{ secrets.DOCS_AWS_SECRET_ACCESS_KEY }}
aws configure set region us-west-2
- name: Check for New Assets
run: |
set -o pipefail
echo "Checking for new assets." |& tee -a $GITHUB_WORKSPACE/deploy.log
echo "aws s3 sync docs/build/assets/ s3://${{ vars.DOCS_DRAFT_S3_BUCKET_NAME }}/langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}/assets/ --size-only --dryrun --no-progress" | tee -a $GITHUB_WORKSPACE/deploy.log
aws s3 sync docs/build/assets/ "s3://${{ vars.DOCS_DRAFT_S3_BUCKET_NAME }}/langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}/assets/" --size-only --dryrun --no-progress | tee $GITHUB_WORKSPACE/assets.log
- name: Determine Standard or Full Publish
id: check_full_publish
run: |
# Determine if a full publish is required because of new assets.
if grep -qE '(upload:|delete:)' "$GITHUB_WORKSPACE/assets.log"; then
echo "New assets. Perform full publish: true" | tee -a "$GITHUB_WORKSPACE/deploy.log"
echo "perform_full_publish=true" >> "$GITHUB_OUTPUT"
else
echo "No new assets. Perform full publish: false" | tee -a "$GITHUB_WORKSPACE/deploy.log"
echo "perform_full_publish=false" >> "$GITHUB_OUTPUT"
fi
- name: Deploy to S3
if: success()
run: |
set -o pipefail
cd docs
mkdir langflow-drafts
mv build langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}
cd langflow-drafts
# Records the repository that originally triggered the build so we can post back
# comments upon clean up of a stale draft if it still has an open pull request.
echo "${{ github.event.repository.full_name }}" > ${{ steps.extract_branch.outputs.draft_directory }}/.github_source_repository
s3_params=(
# Hide upload progress for a cleaner sync log
--no-progress
--delete
--exclude "*"
--include "${{ steps.extract_branch.outputs.draft_directory }}/*"
)
if [[ "${{ steps.check_full_publish.outputs.perform_full_publish }}" == "false" ]]; then
s3_params+=(--size-only)
fi
echo "Deploying draft to S3." |& tee -a $GITHUB_WORKSPACE/deploy.log
echo "aws s3 sync . s3://${{ vars.DOCS_DRAFT_S3_BUCKET_NAME }}/langflow-drafts ${s3_params[@]}" |& tee -a $GITHUB_WORKSPACE/deploy.log
aws s3 sync . "s3://${{ vars.DOCS_DRAFT_S3_BUCKET_NAME }}/langflow-drafts" "${s3_params[@]}" |& tee -a $GITHUB_WORKSPACE/deploy.log
# Update .github_source_repository file metadata to mark last modified time of the draft.
# This will allow us to later determine if a draft is stale and needs to be cleaned up.
echo "Marking last modified time of the draft." |& tee -a $GITHUB_WORKSPACE/deploy.log
echo "aws s3 cp --metadata '{\"touched\": \"now\"}' \
s3://${{ vars.DOCS_DRAFT_S3_BUCKET_NAME }}/langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}/.github_source_repository \
s3://${{ vars.DOCS_DRAFT_S3_BUCKET_NAME }}/langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}/.github_source_repository" \
|& tee -a $GITHUB_WORKSPACE/deploy.log
aws s3 cp --metadata '{ "touched": "now" }' \
s3://${{ vars.DOCS_DRAFT_S3_BUCKET_NAME }}/langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}/.github_source_repository \
s3://${{ vars.DOCS_DRAFT_S3_BUCKET_NAME }}/langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}/.github_source_repository \
|& tee -a $GITHUB_WORKSPACE/deploy.log
- name: Invalidate CloudFront Cache
if: success()
run: |
invalidation_batch="{ \"Paths\": { \"Quantity\": 1, \"Items\": [\"/langflow-drafts/${{ steps.extract_branch.outputs.draft_directory }}/*\"] }, \"CallerReference\": \"langflow-docs-draft-files-$(date +%s)\" }"
echo $invalidation_batch | jq . |& tee -a "$GITHUB_WORKSPACE/deploy.log"
echo "Creating invalidation." |& tee -a "$GITHUB_WORKSPACE/deploy.log"
invalidation_id=$(aws cloudfront create-invalidation --distribution-id "${{ vars.DOCS_DRAFT_CLOUD_FRONT_DISTRIBUTION_ID }}" --invalidation-batch "$invalidation_batch" --query 'Invalidation.Id' --output text |& tee -a "$GITHUB_WORKSPACE/deploy.log")
echo "Awaiting invalidation." |& tee -a "$GITHUB_WORKSPACE/deploy.log"
aws cloudfront wait invalidation-completed --distribution-id "${{ vars.DOCS_DRAFT_CLOUD_FRONT_DISTRIBUTION_ID }}" --id "$invalidation_id" |& tee -a "$GITHUB_WORKSPACE/deploy.log"
echo "Invalidation complete." |& tee -a "$GITHUB_WORKSPACE/deploy.log"
- name: Update Comment
if: ${{ steps.fc.outputs.comment-id != '' }}
uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.fc.outputs.comment-id }}
body: |
Deploy successful! [View draft](${{ steps.draft_url.outputs.url }})
reactions: hooray
- name: Upload Deploy Log
uses: actions/upload-artifact@v6
if: always()
with:
name: deploy.log
path: ${{ github.workspace }}/deploy.log
deploy-storybook .github/workflows/deploy-storybook.yml
View raw YAML
name: Deploy Storybook to GitHub Pages
on:
push:
branches:
- main
paths:
- 'src/frontend/**/*.stories.*'
- 'src/frontend/.storybook/**'
- 'src/frontend/package.json'
- '.github/workflows/deploy-storybook.yml'
workflow_dispatch: # Allow manual trigger
jobs:
deploy:
name: Deploy Storybook
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: src/frontend/package-lock.json
- name: Install dependencies
run: cd src/frontend && npm ci
- name: Build Storybook
run: cd src/frontend && npm run build-storybook
- name: Setup Pages
uses: actions/configure-pages@v4
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: src/frontend/storybook-static
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
deploy_gh-pages .github/workflows/deploy_gh-pages.yml
View raw YAML
name: Deploy to GitHub Pages
on:
workflow_dispatch:
inputs:
branch:
description: "Branch to deploy docs from"
required: false
default: "main"
type: string
jobs:
deploy:
name: Deploy to GitHub Pages
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.branch }}
- uses: actions/setup-node@v6
with:
node-version: 22
cache: npm
cache-dependency-path: ./docs/package-lock.json
- name: Install dependencies
run: cd docs && npm install
- name: Build website
run: cd docs && npm run build
env:
SEGMENT_PUBLIC_WRITE_KEY: ${{ vars.DOCS_PROD_IBM_SEGMENT_PUBLIC_WRITE_KEY }}
# Popular action to deploy to GitHub Pages:
# Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
# Build output to publish to the `gh-pages` branch:
publish_dir: ./docs/build
# The following lines assign commit authorship to the official
# GH-Actions bot for deploys to `gh-pages` branch:
# https://github.com/actions/checkout/issues/13#issuecomment-724415212
# The GH actions bot is used by default if you didn't specify the two fields.
# You can swap them out with your own user credentials.
docker-build matrix .github/workflows/docker-build.yml
View raw YAML
name: Docker Build and Push
run-name: Docker Build and Push @${{ inputs.release_type }} by @${{ github.actor }}
on:
workflow_call:
inputs:
main_version:
required: true
type: string
description: "Main version to tag images with. Required for both main and base releases."
base_version:
required: false
type: string
description: "Base version to tag images with. Required for base release type."
release_type:
required: true
type: string
description: "Release type. One of 'main', 'main-ep', 'base', 'nightly-main', 'nightly-base', 'main-all', 'nightly-main-all'."
pre_release:
required: false
type: boolean
default: false
ref:
required: false
type: string
description: "Ref to check out. If not specified, will default to the main version or current branch."
warning_check:
required: true
type: boolean
description: "Warning - use docker-build-v2 unless you have a very valid reason for using this deprecated workflow. By setting to True, you acknowledge all risks of using a possibly breaking workflow."
default: false
workflow_dispatch:
inputs:
main_version:
description: "Main version to tag images with. Required for both main and base releases."
required: false
type: string
base_version:
description: "Base version to tag images with. Required for base release type."
required: false
type: string
release_type:
description: "Type of release. One of 'main', 'main-ep', 'base', 'nightly-main', 'nightly-base', 'main-all', 'nightly-main-all'."
required: true
type: string
pre_release:
description: "Whether this is a pre-release."
required: false
type: boolean
default: false
ref:
required: false
type: string
description: "Ref to check out. If not specified, will default to the main version or current branch."
warning_check:
description: "Warning - use docker-build-v2 unless you have a very valid reason for using this deprecated workflow. By setting to True, you acknowledge all risks of using a possibly breaking workflow."
required: true
type: boolean
default: false
env:
PYTHON_VERSION: "3.13"
TEST_TAG: "langflowai/langflow:test"
jobs:
get-version:
name: Get Version
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version-input.outputs.version || steps.get-version-base.outputs.version || steps.get-version-main.outputs.version }}
steps:
- name: Verify warning check
run: |
if [[ "${{ inputs.warning_check }}" == "false" ]]; then
echo "Warning - use docker-build-v2 unless you have a very valid reason for using this deprecated workflow. By setting to True, you acknowledge all risks of using a possibly breaking workflow."
exit 1
else
echo "User has acknowledged the risks of using this deprecated workflow. Proceeding with build."
fi
- name: Verify a main version exists
if: ${{ inputs.main_version == '' }}
run: |
# due to our how we split packages, we need to have a main version to check out.
echo "Must specify a main version to check out."
exit 1
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || inputs.main_version || github.ref }}
persist-credentials: true
- name: Get Version to Tag
if: ${{ inputs.main_version != '' }}
id: get-version-input
run: |
# Produces the versions we will use to tag the docker images with.
if [[ "${{ inputs.release_type }}" == "base" && "${{ inputs.base_version }}" == '' ]]; then
echo "Must specify a base version for base release type."
exit 1
fi
if [[ "${{ inputs.release_type }}" == "nightly-base" && "${{ inputs.base_version }}" == '' ]]; then
echo "Must specify a base version for nightly-base release type."
exit 1
fi
if [[ ("${{ inputs.release_type }}" == "main" || "${{ inputs.release_type }}" == "main-all") && "${{ inputs.main_version }}" == '' ]]; then
echo "Must specify a main version for main release type."
exit 1
fi
if [[ "${{ inputs.release_type }}" == "main-ep" && "${{ inputs.main_version }}" == '' ]]; then
echo "Must specify a main version for main-ep release type."
exit 1
fi
if [[ ("${{ inputs.release_type }}" == "nightly-main" || "${{ inputs.release_type }}" == "nightly-main-all") && "${{ inputs.main_version }}" == '' ]]; then
echo "Must specify a main version for nightly-main release type."
exit 1
fi
if [[ "${{ inputs.release_type }}" == "base" || "${{ inputs.release_type }}" == "nightly-base" ]]; then
version=${{ inputs.base_version }}
echo "base version=${{ inputs.base_version }}"
echo version=$version
echo version=$version >> $GITHUB_OUTPUT
elif [[ "${{ inputs.release_type }}" == "main" || "${{ inputs.release_type }}" == "main-ep" || "${{ inputs.release_type }}" == "nightly-main" || "${{ inputs.release_type }}" == "nightly-main-all" ]]; then
version=${{ inputs.main_version }}
echo version=$version
echo version=$version >> $GITHUB_OUTPUT
else
echo "No version or ref specified. Exiting the workflow."
exit 1
fi
- name: Get Version Base
if: ${{ inputs.base_version == '' && (inputs.release_type == 'base' || inputs.release_type == 'nightly-base') }}
id: get-version-base
run: |
version=$(uv tree | grep 'langflow-base' | awk '{print $3}' | sed 's/^v//' | head -n 1)
if [ -z "$version" ]; then
echo "Failed to extract version from uv tree output"
exit 1
fi
echo version=$version
echo version=$version >> $GITHUB_OUTPUT
- name: Get Version Main
if: ${{ inputs.main_version == '' && (inputs.release_type == 'main' || inputs.release_type == 'main-ep' || inputs.release_type == 'nightly-main' || inputs.release_type == 'main-all' || inputs.release_type == 'nightly-main-all') }}
id: get-version-main
run: |
version=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | awk '{print $2}' | sed 's/^v//')
echo version=$version
echo version=$version >> $GITHUB_OUTPUT
setup:
runs-on: ubuntu-latest
needs: get-version
outputs:
docker_tags: ${{ steps.set-vars.outputs.docker_tags }}
ghcr_tags: ${{ steps.set-vars.outputs.ghcr_tags }}
file: ${{ steps.set-vars.outputs.file }}
steps:
- name: Set Dockerfile and Tags
id: set-vars
run: |
nightly_suffix=''
if [[ "${{ inputs.release_type }}" == "nightly-base" || "${{ inputs.release_type }}" == "nightly-main" || "${{ inputs.release_type }}" == "nightly-main-all" ]]; then
nightly_suffix="-nightly"
fi
if [[ "${{ inputs.release_type }}" == "base" || "${{ inputs.release_type }}" == "nightly-base" ]]; then
# LANGFLOW-BASE RELEASE
echo "docker_tags=langflowai/langflow${nightly_suffix}:base-${{ needs.get-version.outputs.version }},langflowai/langflow${nightly_suffix}:base-latest" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow${nightly_suffix}:base-${{ needs.get-version.outputs.version }},ghcr.io/langflow-ai/langflow${nightly_suffix}:base-latest" >> $GITHUB_OUTPUT
echo "file=./docker/build_and_push_base.Dockerfile" >> $GITHUB_OUTPUT
else
if [[ "${{ inputs.pre_release }}" == "true" ]]; then
# LANGFLOW-MAIN PRE-RELEASE
echo "docker_tags=langflowai/langflow${nightly_suffix}:${{ needs.get-version.outputs.version }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow${nightly_suffix}:${{ needs.get-version.outputs.version }}" >> $GITHUB_OUTPUT
echo "file=./docker/build_and_push.Dockerfile" >> $GITHUB_OUTPUT
elif [[ "${{ inputs.release_type }}" == "main-ep" ]]; then
# LANGFLOW-MAIN (ENTRYPOINT) RELEASE
echo "docker_tags=langflowai/langflow-ep${nightly_suffix}:${{ needs.get-version.outputs.version }},langflowai/langflow-ep${nightly_suffix}:latest" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-ep${nightly_suffix}:${{ needs.get-version.outputs.version }},ghcr.io/langflow-ai/langflow-ep${nightly_suffix}:latest" >> $GITHUB_OUTPUT
echo "file=./docker/build_and_push_ep.Dockerfile" >> $GITHUB_OUTPUT
elif [[ "${{ inputs.release_type }}" == "main" || "${{ inputs.release_type }}" == "nightly-main" ]]; then
# LANGFLOW-MAIN RELEASE
echo "docker_tags=langflowai/langflow${nightly_suffix}:${{ needs.get-version.outputs.version }},langflowai/langflow${nightly_suffix}:latest" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow${nightly_suffix}:${{ needs.get-version.outputs.version }},ghcr.io/langflow-ai/langflow${nightly_suffix}:latest" >> $GITHUB_OUTPUT
echo "file=./docker/build_and_push.Dockerfile" >> $GITHUB_OUTPUT
elif [[ "${{ inputs.release_type }}" == "main-all" || "${{ inputs.release_type }}" == "nightly-main-all" ]]; then
# LANGFLOW-MAIN (ALL OPTIONAL DEPS) RELEASE
echo "docker_tags=langflowai/langflow-all${nightly_suffix}:${{ needs.get-version.outputs.version }},langflowai/langflow-all${nightly_suffix}:latest" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-all${nightly_suffix}:${{ needs.get-version.outputs.version }},ghcr.io/langflow-ai/langflow-all${nightly_suffix}:latest" >> $GITHUB_OUTPUT
echo "file=./docker/build_and_push_with_extras.Dockerfile" >> $GITHUB_OUTPUT
else
echo "Invalid release type. Exiting the workflow."
exit 1
fi
fi
build:
runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
needs: [get-version, setup]
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || inputs.main_version || github.ref }}
persist-credentials: true
- 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: |
if [[ "${{ inputs.release_type }}" == "base" || "${{ inputs.release_type }}" == "nightly-base" ]]; then
uv sync --directory src/backend/base --no-dev --no-sources
else
uv sync --no-dev --no-sources
fi
- name: Docker System Info and Cleanup
run: |
echo "=== Docker System Usage Before Cleanup ==="
docker system df || true
docker buildx du || true
echo "=== Cleaning up Docker System ==="
docker system prune -af --volumes || true
docker buildx prune -af || true
echo "=== Docker System Usage After Cleanup ==="
docker system df || true
docker buildx du || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
driver: docker-container
driver-opts: |
image=moby/buildkit:v0.22.0
network=host
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and Push to Docker Hub
uses: Wandalen/wretry.action@master
with:
action: docker/build-push-action@v6
with: |
context: .
push: true
file: ${{ needs.setup.outputs.file }}
tags: ${{ needs.setup.outputs.docker_tags }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Login to Github Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to Github Container Registry
uses: Wandalen/wretry.action@master
with:
action: docker/build-push-action@v6
with: |
context: .
push: true
file: ${{ needs.setup.outputs.file }}
tags: ${{ needs.setup.outputs.ghcr_tags }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
build_components:
if: ${{ inputs.release_type == 'main' }}
runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
permissions:
packages: write
needs: [build, get-version]
strategy:
matrix:
component:
[docker-backend, docker-frontend, ghcr-backend, ghcr-frontend]
include:
- component: docker-backend
dockerfile: ./docker/build_and_push_backend.Dockerfile
tags: langflowai/langflow-backend:${{ needs.get-version.outputs.version }},langflowai/langflow-backend:latest
langflow_image: langflowai/langflow:${{ needs.get-version.outputs.version }}
- component: docker-frontend
dockerfile: ./docker/frontend/build_and_push_frontend.Dockerfile
tags: langflowai/langflow-frontend:${{ needs.get-version.outputs.version }},langflowai/langflow-frontend:latest
langflow_image: langflowai/langflow:${{ needs.get-version.outputs.version }}
- component: ghcr-backend
dockerfile: ./docker/build_and_push_backend.Dockerfile
tags: ghcr.io/langflow-ai/langflow-backend:${{ needs.get-version.outputs.version }},ghcr.io/langflow-ai/langflow-backend:latest
langflow_image: ghcr.io/langflow-ai/langflow:${{ needs.get-version.outputs.version }}
- component: ghcr-frontend
dockerfile: ./docker/frontend/build_and_push_frontend.Dockerfile
tags: ghcr.io/langflow-ai/langflow-frontend:${{ needs.get-version.outputs.version }},ghcr.io/langflow-ai/langflow-frontend:latest
langflow_image: ghcr.io/langflow-ai/langflow:${{ needs.get-version.outputs.version }}
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || inputs.main_version || github.ref }}
- name: Docker System Info and Cleanup
run: |
echo "=== Docker System Usage Before Cleanup ==="
docker system df || true
docker buildx du || true
echo "=== Cleaning up Docker System ==="
docker system prune -af --volumes || true
docker buildx prune -af || true
echo "=== Docker System Usage After Cleanup ==="
docker system df || true
docker buildx du || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
version: latest
driver: docker-container
driver-opts: |
image=moby/buildkit:v0.22.0
network=host
- name: Login to Docker Hub
if: ${{ matrix.component == 'docker-backend' }} || ${{ matrix.component == 'docker-frontend' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Container Registry
if: ${{ matrix.component == 'ghcr-backend' }} || ${{ matrix.component == 'ghcr-frontend' }}
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Wait for propagation (for backend)
run: sleep 120
- name: Build and push ${{ matrix.component }}
uses: Wandalen/wretry.action@master
with:
action: docker/build-push-action@v6
with: |
context: .
push: true
build-args: |
LANGFLOW_IMAGE=${{ matrix.langflow_image }}
file: ${{ matrix.dockerfile }}
tags: ${{ matrix.tags }}
# provenance: false will result in a single manifest for all platforms which makes the image pullable from arm64 machines via the emulation (e.g. Apple Silicon machines)
provenance: false
restart-space:
name: Restart HuggingFace Spaces
if: ${{ inputs.release_type == 'main' }}
runs-on: ubuntu-latest
needs: [build, get-version]
strategy:
matrix:
python-version:
- "3.13"
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || inputs.main_version || github.ref }}
- name: "Setup Environment"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Restart HuggingFace Spaces Build
run: |
uv run ./scripts/factory_restart_space.py --space "Langflow/Langflow" --token ${{ secrets.HUGGINGFACE_API_TOKEN }}
docker-build-v2 matrix .github/workflows/docker-build-v2.yml
View raw YAML
name: Docker Build and Push v2
run-name: Docker Build and Push @${{ inputs.release_type }} by @${{ github.actor }}
on:
workflow_call:
inputs:
release_type:
required: true
type: string
description: "Release type. One of 'main', 'main-backend', 'main-frontend', 'main-ep', 'base', 'main-all'."
pre_release:
required: false
type: boolean
default: false
ref:
required: true
type: string
description: "Ref to check out (branch, tag, or commit). This is required -- it specifies where the source code for the release is located."
push_to_registry:
required: false
type: boolean
default: true
description: "Whether to push images to registries. Set to false for testing builds without publishing."
workflow_dispatch:
inputs:
release_type:
description: "Type of release. One of 'main', 'main-backend', 'main-frontend', 'main-ep', 'base', 'main-all'."
required: true
type: choice
options:
- main
- main-backend
- main-frontend
- main-ep
- base
- main-all
pre_release:
description: "Whether this is a pre-release."
required: false
type: boolean
default: false
ref:
required: true
type: string
description: "Ref to check out (branch, tag, or commit). This is required -- it specifies where the source code for the release is located."
push_to_registry:
description: "Whether to push images to registries. Set to false for testing builds without publishing."
required: false
type: boolean
default: false
env:
PYTHON_VERSION: "3.13"
jobs:
determine-base-version:
name: Determine Base Version
if: ${{ inputs.release_type == 'base' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- 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: Determine version
id: version
run: |
version=$(uv tree 2>/dev/null | grep '^langflow-base' | cut -d' ' -f2 | sed 's/^v//')
echo "Base version from pyproject.toml: $version"
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -E '^base-.*\.rc[0-9]+' | grep -vE '\-(amd64|arm64)$' | sed 's/^base-//' | 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://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -E '^base-' | grep -vE '\-(amd64|arm64)$' | grep -v 'latest' | sed 's/^base-//' | sort -V | tail -n 1)
echo "Latest base release version: $last_released_version"
fi
if [ "$version" = "$last_released_version" ]; then
echo "Base docker version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 0
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
determine-main-version:
name: Determine Main Version
if: ${{ inputs.release_type != 'base' }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- 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: Determine version
id: version
run: |
version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | cut -d' ' -f2 | sed 's/^v//')
echo "Main version from pyproject.toml: $version"
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -E '\.rc[0-9]+' | grep -v '^base-' | grep -vE '\-(amd64|arm64)$' | 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://registry.hub.docker.com/v2/repositories/langflowai/langflow/tags?page_size=100" | jq -r '.results[].name' | grep -v '^base-' | grep -vE '\-(amd64|arm64)$' | grep -v 'latest' | sort -V | tail -n 1)
echo "Latest main release version: $last_released_version"
fi
if [ "$version" = "$last_released_version" ]; then
echo "Main docker version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 0
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
build-base:
name: Build Base Package
needs: [determine-base-version]
if: ${{ inputs.release_type == 'base' && needs.determine-base-version.outputs.skipped == 'false' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Set tags
id: tags
run: |
version="${{ needs.determine-base-version.outputs.version }}"
echo "docker_tags=langflowai/langflow:base-${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow:base-${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_base.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_base.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
build-main:
name: Build Main Package
needs: [determine-main-version]
if: ${{ inputs.release_type == 'main' && needs.determine-main-version.outputs.skipped == 'false' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Set tags
id: tags
run: |
version="${{ needs.determine-main-version.outputs.version }}"
echo "docker_tags=langflowai/langflow:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
build-main-backend:
name: Build Main Backend Package
needs: [determine-main-version]
if: ${{ inputs.release_type == 'main-backend' && needs.determine-main-version.outputs.skipped == 'false' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Set tags
id: tags
run: |
version="${{ needs.determine-main-version.outputs.version }}"
echo "docker_tags=langflowai/langflow-backend:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-backend:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
build-args: |
LANGFLOW_IMAGE=langflowai/langflow:${{ steps.version.outputs.version }}-${{ matrix.arch }}
file: ./docker/build_and_push_backend.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
build-args: |
LANGFLOW_IMAGE=ghcr.io/langflow-ai/langflow:${{ steps.version.outputs.version }}-${{ matrix.arch }}
file: ./docker/build_and_push_backend.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
build-main-frontend:
name: Build Main Frontend Package
needs: [determine-main-version]
if: ${{ inputs.release_type == 'main-frontend' && needs.determine-main-version.outputs.skipped == 'false' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Set tags
id: tags
run: |
version="${{ needs.determine-main-version.outputs.version }}"
echo "docker_tags=langflowai/langflow-frontend:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-frontend:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/frontend/build_and_push_frontend.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/frontend/build_and_push_frontend.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
build-main-ep:
name: Build Main EP Package
needs: [determine-main-version]
if: ${{ inputs.release_type == 'main-ep' && needs.determine-main-version.outputs.skipped == 'false' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Set tags
id: tags
run: |
version="${{ needs.determine-main-version.outputs.version }}"
echo "docker_tags=langflowai/langflow-ep:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-ep:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_ep.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_ep.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
build-main-all:
name: Build Main All Package
needs: [determine-main-version]
if: ${{ inputs.release_type == 'main-all' && needs.determine-main-version.outputs.skipped == 'false' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Set tags
id: tags
run: |
version="${{ needs.determine-main-version.outputs.version }}"
echo "docker_tags=langflowai/langflow-all:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-all:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_with_extras.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_with_extras.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
create-manifest:
name: Create Multi-Arch Manifest
needs:
[
build-base,
build-main,
build-main-backend,
build-main-frontend,
build-main-ep,
build-main-all,
determine-base-version,
determine-main-version,
]
runs-on: ubuntu-latest
if: ${{ always() && inputs.push_to_registry && (needs.build-base.result == 'success' || needs.build-main.result == 'success' || needs.build-main-backend.result == 'success' || needs.build-main-frontend.result == 'success' || needs.build-main-ep.result == 'success' || needs.build-main-all.result == 'success') }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Get version
id: version
run: |
if [[ "${{ inputs.release_type }}" == "base" ]]; then
version=$(uv tree 2>/dev/null | grep 'langflow-base' | awk '{print $3}' | sed 's/^v//' | head -n 1)
else
version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | cut -d' ' -f2 | sed 's/^v//')
fi
echo "Using version: $version"
echo version=$version >> $GITHUB_OUTPUT
- name: Set tags
id: tags
run: |
version="${{ needs.determine-main-version.outputs.version }}"
case "${{ inputs.release_type }}" in
"base")
version="${{ needs.determine-base-version.outputs.version }}"
if [[ "${{ inputs.pre_release }}" == "true" ]]; then
echo "final_tags=langflowai/langflow:base-${version},ghcr.io/langflow-ai/langflow:base-${version}" >> $GITHUB_OUTPUT
else
echo "final_tags=langflowai/langflow:base-${version},langflowai/langflow:base-latest,ghcr.io/langflow-ai/langflow:base-${version},ghcr.io/langflow-ai/langflow:base-latest" >> $GITHUB_OUTPUT
fi
echo "arch_base=langflowai/langflow:base-${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow:base-${version}" >> $GITHUB_OUTPUT
;;
"main")
if [[ "${{ inputs.pre_release }}" == "true" ]]; then
echo "final_tags=langflowai/langflow:${version},ghcr.io/langflow-ai/langflow:${version}" >> $GITHUB_OUTPUT
else
echo "final_tags=langflowai/langflow:${version},langflowai/langflow:latest,ghcr.io/langflow-ai/langflow:${version},ghcr.io/langflow-ai/langflow:latest" >> $GITHUB_OUTPUT
fi
echo "arch_base=langflowai/langflow:${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow:${version}" >> $GITHUB_OUTPUT
;;
"main-backend")
if [[ "${{ inputs.pre_release }}" == "true" ]]; then
echo "final_tags=langflowai/langflow-backend:${version},ghcr.io/langflow-ai/langflow-backend:${version}" >> $GITHUB_OUTPUT
else
echo "final_tags=langflowai/langflow-backend:${version},langflowai/langflow-backend:latest,ghcr.io/langflow-ai/langflow-backend:${version},ghcr.io/langflow-ai/langflow-backend:latest" >> $GITHUB_OUTPUT
fi
echo "arch_base=langflowai/langflow-backend:${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow-backend:${version}" >> $GITHUB_OUTPUT
;;
"main-frontend")
if [[ "${{ inputs.pre_release }}" == "true" ]]; then
echo "final_tags=langflowai/langflow-frontend:${version},ghcr.io/langflow-ai/langflow-frontend:${version}" >> $GITHUB_OUTPUT
else
echo "final_tags=langflowai/langflow-frontend:${version},langflowai/langflow-frontend:latest,ghcr.io/langflow-ai/langflow-frontend:${version},ghcr.io/langflow-ai/langflow-frontend:latest" >> $GITHUB_OUTPUT
fi
echo "arch_base=langflowai/langflow-frontend:${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow-frontend:${version}" >> $GITHUB_OUTPUT
;;
"main-ep")
if [[ "${{ inputs.pre_release }}" == "true" ]]; then
echo "final_tags=langflowai/langflow-ep:${version},ghcr.io/langflow-ai/langflow-ep:${version}" >> $GITHUB_OUTPUT
else
echo "final_tags=langflowai/langflow-ep:${version},langflowai/langflow-ep:latest,ghcr.io/langflow-ai/langflow-ep:${version},ghcr.io/langflow-ai/langflow-ep:latest" >> $GITHUB_OUTPUT
fi
echo "arch_base=langflowai/langflow-ep:${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow-ep:${version}" >> $GITHUB_OUTPUT
;;
"main-all")
if [[ "${{ inputs.pre_release }}" == "true" ]]; then
echo "final_tags=langflowai/langflow-all:${version},ghcr.io/langflow-ai/langflow-all:${version}" >> $GITHUB_OUTPUT
else
echo "final_tags=langflowai/langflow-all:${version},langflowai/langflow-all:latest,ghcr.io/langflow-ai/langflow-all:${version},ghcr.io/langflow-ai/langflow-all:latest" >> $GITHUB_OUTPUT
fi
echo "arch_base=langflowai/langflow-all:${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow-all:${version}" >> $GITHUB_OUTPUT
;;
*)
echo "Error: Invalid release_type: ${{ inputs.release_type }}" >&2
exit 1
;;
esac
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Create and push multi-arch manifests
run: |
# Split tags and create manifests for each
IFS=',' read -ra TAGS <<< "${{ steps.tags.outputs.final_tags }}"
for tag in "${TAGS[@]}"; do
echo "Creating manifest for $tag"
# Determine architecture-specific tags
if [[ "$tag" == *"langflowai"* ]]; then
amd64_tag="${{ steps.tags.outputs.arch_base }}-amd64"
arm64_tag="${{ steps.tags.outputs.arch_base }}-arm64"
else
amd64_tag="${{ steps.tags.outputs.ghcr_arch_base }}-amd64"
arm64_tag="${{ steps.tags.outputs.ghcr_arch_base }}-arm64"
fi
docker buildx imagetools create \
--tag "$tag" \
"$amd64_tag" \
"$arm64_tag"
done
docker-nightly-build matrix .github/workflows/docker-nightly-build.yml
View raw YAML
name: Docker Nightly Build and Push
run-name: Docker Nightly Build @${{ inputs.release_type }} by @${{ github.actor }}
on:
workflow_call:
inputs:
release_type:
required: true
type: string
description: "Nightly release type. One of 'nightly-main', 'nightly-base', 'nightly-main-all'."
ref:
required: true
type: string
description: "Ref to check out (branch, tag, or commit). This is required -- it specifies where the source code for the release is located."
push_to_registry:
required: false
type: boolean
default: true
description: "Whether to push images to registries. Set to false for testing builds without publishing."
workflow_dispatch:
inputs:
release_type:
description: "Nightly release type. One of 'nightly-main', 'nightly-base', 'nightly-main-all'."
required: true
type: choice
options:
- nightly-main
- nightly-base
- nightly-main-all
ref:
required: true
type: string
description: "Ref to check out (branch, tag, or commit). This is required -- it specifies where the source code for the release is located. Note that if running via Github Actions, this tag (formatted as, e.g., v1.5.1.dev36) must be manually created and pushed to a branch prior to running the workflow."
push_to_registry:
description: "Whether to push images to registries. Set to false for testing builds without publishing."
required: false
type: boolean
default: false
env:
PYTHON_VERSION: "3.13"
jobs:
build-nightly-base:
name: Build Nightly Base Package
if: ${{ inputs.release_type == 'nightly-base' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- 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: Get version for tagging
id: version
run: |
echo "Extracting base version from pyproject.toml"
version=$(uv tree 2>/dev/null | grep 'langflow-base' | awk '{print $3}' | sed 's/^v//' | head -n 1)
# Verify nightly tag format
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$ ]]; then
echo "Base version format is incorrect. Must be in the format (e.g.) v1.5.1.dev36"
exit 1
fi
echo "Using version: $version"
echo version=$version >> $GITHUB_OUTPUT
- name: Set nightly tags
id: tags
run: |
version="${{ steps.version.outputs.version }}"
echo "docker_tags=langflowai/langflow-nightly:base-${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-nightly:base-${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_base.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_base.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
build-nightly-main:
name: Build Nightly Main Package
if: ${{ inputs.release_type == 'nightly-main' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- 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: Get version for tagging
id: version
run: |
echo "Extracting main version from pyproject.toml"
version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | cut -d' ' -f2 | sed 's/^v//')
# Verify nightly tag format
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$ ]]; then
echo "Main version format is incorrect. Must be in the format (e.g.) v1.5.1.dev36"
exit 1
fi
echo "Using version: $version"
echo version=$version >> $GITHUB_OUTPUT
- name: Set nightly tags
id: tags
run: |
version="${{ steps.version.outputs.version }}"
echo "docker_tags=langflowai/langflow-nightly:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-nightly:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
build-nightly-main-all:
name: Build Nightly Main All Package
if: ${{ inputs.release_type == 'nightly-main-all' }}
strategy:
matrix:
include:
- arch: amd64
runner: [Langflow-runner]
- arch: arm64
runner:
[self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
runs-on: ${{ matrix.runner }}
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- 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: Get version for tagging
id: version
run: |
echo "Extracting main version from pyproject.toml"
version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | cut -d' ' -f2 | sed 's/^v//')
# Verify nightly tag format
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.dev[0-9]+$ ]]; then
echo "Main version format is incorrect. Must be in the format (e.g.) v1.5.1.dev36"
exit 1
fi
echo "Using version: $version"
echo version=$version >> $GITHUB_OUTPUT
- name: Set nightly tags
id: tags
run: |
version="${{ steps.version.outputs.version }}"
echo "docker_tags=langflowai/langflow-nightly-all:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
echo "ghcr_tags=ghcr.io/langflow-ai/langflow-nightly-all:${version}-${{ matrix.arch }}" >> $GITHUB_OUTPUT
- name: Docker cleanup
run: |
docker system prune -af --volumes || true
docker buildx prune -af || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Build and push to Docker Hub
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_with_extras.Dockerfile
tags: ${{ steps.tags.outputs.docker_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push to GitHub Container Registry
uses: docker/build-push-action@v6
with:
context: .
push: ${{ inputs.push_to_registry }}
file: ./docker/build_and_push_with_extras.Dockerfile
tags: ${{ steps.tags.outputs.ghcr_tags }}
platforms: linux/${{ matrix.arch }}
provenance: false
cache-from: type=gha
cache-to: type=gha,mode=max
create-nightly-manifest:
name: Create Multi-Arch Nightly Manifest
needs: [build-nightly-base, build-nightly-main, build-nightly-main-all]
runs-on: ubuntu-latest
if: ${{ always() && inputs.push_to_registry && (needs.build-nightly-base.result == 'success' || needs.build-nightly-main.result == 'success' || needs.build-nightly-main-all.result == 'success') }}
steps:
- 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: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref }}
- name: Get version
id: version
run: |
if [[ "${{ inputs.release_type }}" == "nightly-base" ]]; then
version=$(uv tree 2>/dev/null | grep 'langflow-base' | awk '{print $3}' | sed 's/^v//' | head -n 1)
else
version=$(uv tree 2>/dev/null | grep '^langflow' | grep -v '^langflow-base' | cut -d' ' -f2 | sed 's/^v//')
fi
echo "Using version: $version"
echo version=$version >> $GITHUB_OUTPUT
- name: Set nightly tags
id: tags
run: |
version="${{ steps.version.outputs.version }}"
case "${{ inputs.release_type }}" in
"nightly-base")
echo "final_tags=langflowai/langflow-nightly:base-${version},langflowai/langflow-nightly:base-latest,ghcr.io/langflow-ai/langflow-nightly:base-${version},ghcr.io/langflow-ai/langflow-nightly:base-latest" >> $GITHUB_OUTPUT
echo "arch_base=langflowai/langflow-nightly:base-${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow-nightly:base-${version}" >> $GITHUB_OUTPUT
;;
"nightly-main")
echo "final_tags=langflowai/langflow-nightly:${version},langflowai/langflow-nightly:latest,ghcr.io/langflow-ai/langflow-nightly:${version},ghcr.io/langflow-ai/langflow-nightly:latest" >> $GITHUB_OUTPUT
echo "arch_base=langflowai/langflow-nightly:${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow-nightly:${version}" >> $GITHUB_OUTPUT
;;
"nightly-main-all")
echo "final_tags=langflowai/langflow-nightly-all:${version},langflowai/langflow-nightly-all:latest,ghcr.io/langflow-ai/langflow-nightly-all:${version},ghcr.io/langflow-ai/langflow-nightly-all:latest" >> $GITHUB_OUTPUT
echo "arch_base=langflowai/langflow-nightly-all:${version}" >> $GITHUB_OUTPUT
echo "ghcr_arch_base=ghcr.io/langflow-ai/langflow-nightly-all:${version}" >> $GITHUB_OUTPUT
;;
*)
echo "Error: Invalid release_type: ${{ inputs.release_type }}" >&2
exit 1
;;
esac
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.TEMP_GHCR_TOKEN}}
- name: Create and push multi-arch nightly manifests
run: |
# Split tags and create manifests for each
IFS=',' read -ra TAGS <<< "${{ steps.tags.outputs.final_tags }}"
for tag in "${TAGS[@]}"; do
echo "Creating manifest for $tag"
# Determine architecture-specific tags
if [[ "$tag" == *"langflowai"* ]]; then
amd64_tag="${{ steps.tags.outputs.arch_base }}-amd64"
arm64_tag="${{ steps.tags.outputs.arch_base }}-arm64"
else
amd64_tag="${{ steps.tags.outputs.ghcr_arch_base }}-amd64"
arm64_tag="${{ steps.tags.outputs.ghcr_arch_base }}-arm64"
fi
docker buildx imagetools create \
--tag "$tag" \
"$amd64_tag" \
"$arm64_tag"
done
docker_test .github/workflows/docker_test.yml
View raw YAML
name: Test Docker images
on:
workflow_call:
secrets:
DOCKERHUB_USERNAME:
required: true
DOCKERHUB_TOKEN:
required: true
workflow_dispatch:
env:
POETRY_VERSION: "1.8.2"
jobs:
test-docker:
runs-on: [self-hosted, linux, ARM64, langflow-ai-arm64-40gb-ephemeral]
name: Test docker images
steps:
- uses: actions/checkout@v6
- name: Set container runtime
run: |
if command -v docker &> /dev/null; then
echo "CONTAINER_RUNTIME=docker" >> $GITHUB_ENV
elif command -v podman &> /dev/null; then
echo "CONTAINER_RUNTIME=podman" >> $GITHUB_ENV
else
echo "Error: Neither docker nor podman found"
exit 1
fi
echo "Using container runtime: ${{ env.CONTAINER_RUNTIME }}"
- name: Docker System Info and Cleanup
run: |
echo "=== Docker System Usage Before Cleanup ==="
${{ env.CONTAINER_RUNTIME }} system df || true
if command -v docker &> /dev/null; then
docker buildx du || true
fi
echo "=== Cleaning up Docker System ==="
${{ env.CONTAINER_RUNTIME }} system prune -af --volumes || true
if command -v docker &> /dev/null; then
docker buildx prune -af || true
fi
echo "=== Docker System Usage After Cleanup ==="
${{ env.CONTAINER_RUNTIME }} system df || true
if command -v docker &> /dev/null; then
docker buildx du || true
fi
- name: Login to Docker Hub
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository)
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build image
run: |
${{ env.CONTAINER_RUNTIME }} build -t langflowai/langflow:latest-dev \
-f docker/build_and_push.Dockerfile \
.
- name: Test image
run: |
expected_version=$(cat pyproject.toml | grep version | head -n 1 | cut -d '"' -f 2)
version=$(${{ env.CONTAINER_RUNTIME }} run --rm --entrypoint bash langflowai/langflow:latest-dev -c "python -c 'from langflow.utils.version import get_version_info; print(get_version_info()[\"version\"])'")
if [ "$expected_version" != "$version" ]; then
echo "Expected version: $expected_version"
echo "Actual version: $version"
exit 1
fi
- name: Cleanup after main image
run: |
echo "=== Disk usage before cleanup ==="
df -h /
${{ env.CONTAINER_RUNTIME }} rmi langflowai/langflow:latest-dev || true
${{ env.CONTAINER_RUNTIME }} system prune -af || true
if command -v docker &> /dev/null; then
docker buildx prune -af || true
fi
echo "=== Disk usage after cleanup ==="
df -h /
- name: Build backend image
run: |
${{ env.CONTAINER_RUNTIME }} build -t langflowai/langflow-backend:latest-dev \
--build-arg LANGFLOW_IMAGE=langflowai/langflow:latest-dev \
-f docker/build_and_push_backend.Dockerfile \
.
- name: Test backend image
run: |
expected_version=$(cat src/backend/base/pyproject.toml | grep version | head -n 1 | cut -d '"' -f 2)
version=$(${{ env.CONTAINER_RUNTIME }} run --rm --entrypoint bash langflowai/langflow-backend:latest-dev -c "python -c 'from langflow.utils.version import get_version_info; print(get_version_info()[\"version\"])'")
if [ "$expected_version" != "$version" ]; then
echo "Expected version: $expected_version"
echo "Actual version: $version"
exit 1
fi
- name: Cleanup after backend image
run: |
echo "=== Disk usage before cleanup ==="
df -h /
${{ env.CONTAINER_RUNTIME }} rmi langflowai/langflow-backend:latest-dev || true
${{ env.CONTAINER_RUNTIME }} system prune -af || true
if command -v docker &> /dev/null; then
docker buildx prune -af || true
fi
echo "=== Disk usage after cleanup ==="
df -h /
- name: Build frontend image
run: |
${{ env.CONTAINER_RUNTIME }} build -t langflowai/langflow-frontend:latest-dev \
-f docker/frontend/build_and_push_frontend.Dockerfile \
.
docs-update-openapi perms .github/workflows/docs-update-openapi.yml
View raw YAML
name: Update OpenAPI Spec
on:
schedule:
- cron: "0 20 * * 1" # Monday 4pm EST
workflow_dispatch: # Allow manual trigger
permissions:
contents: write
pull-requests: write
jobs:
check-openapi-updates:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install jq
run: sudo apt-get install -y jq
- name: Check if there is already an open pull request
id: check_pull_request
run: |
if [[ -n $(gh pr list --state open --repo ${{ github.repository }} | grep "docs: OpenAPI spec") ]]; then
echo "There is already an open PR with updates to the OpenAPI spec. Merge or close that PR first. Skipping."
echo "pr_exists=true" >> $GITHUB_OUTPUT
else
echo "pr_exists=false" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run Langflow container and get OpenAPI spec
if: steps.check_pull_request.outputs.pr_exists != 'true'
run: |
# Start the container in the background
docker run -d --name langflow -p 7860:7860 langflowai/langflow:latest
# Wait for the service to be ready (adjust timeout as needed)
echo "Waiting for Langflow to start..."
timeout=60
elapsed=0
while ! curl -s http://localhost:7860/health > /dev/null; do
sleep 2
elapsed=$((elapsed+2))
if [ "$elapsed" -ge "$timeout" ]; then
echo "Timed out waiting for Langflow to start"
exit 1
fi
echo "Still waiting... ($elapsed seconds)"
done
# Get the OpenAPI spec and save to file
echo "Fetching OpenAPI spec..."
curl -s http://localhost:7860/openapi.json > docs/openapi/new_openapi.json
# Verify file was created
ls -la docs/openapi/new_openapi.json
# stop the container when done
docker stop langflow
- name: Compare OpenAPI files
if: steps.check_pull_request.outputs.pr_exists != 'true'
id: compare
run: |
# Extract versions
NEW_VERSION=$(jq -r '.info.version' docs/openapi/new_openapi.json)
CURRENT_VERSION=$(jq -r '.info.version' docs/openapi/openapi.json)
echo "Current version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Compare file content (normalize by sorting keys)
jq --sort-keys . docs/openapi/new_openapi.json > docs/openapi/sorted_new.json
jq --sort-keys . docs/openapi/openapi.json > docs/openapi/sorted_current.json
if ! cmp -s docs/openapi/sorted_new.json docs/openapi/sorted_current.json; then
echo "OpenAPI spec content has changed."
# Clean the new spec for ReDoc formatting
echo "Cleaning OpenAPI spec formatting..."
python3 docs/scripts/clean_openapi_formatting.py docs/openapi/new_openapi.json
# Compare versions (assuming semantic versioning)
if [ "$(printf '%s\n' "$CURRENT_VERSION" "$NEW_VERSION" | sort -V | tail -n1)" == "$NEW_VERSION" ] && [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
echo "New version detected. Creating PR."
echo "NEEDS_UPDATE=true" >> $GITHUB_ENV
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
echo "UPDATE_REASON=version upgraded from $CURRENT_VERSION to $NEW_VERSION" >> $GITHUB_ENV
else
echo "File content changed but version remains the same. Creating PR."
echo "NEEDS_UPDATE=true" >> $GITHUB_ENV
echo "NEW_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV
echo "UPDATE_REASON=content updated without version change" >> $GITHUB_ENV
fi
cat docs/openapi/new_openapi.json | jq > docs/openapi/openapi.json
else
echo "No changes detected in OpenAPI spec content."
echo "NEEDS_UPDATE=false" >> $GITHUB_ENV
fi
# Clean up
rm docs/openapi/new_openapi.json docs/openapi/sorted_new.json docs/openapi/sorted_current.json
- name: Create Pull Request
if: env.NEEDS_UPDATE == 'true' && steps.check_pull_request.outputs.pr_exists != 'true'
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "docs: OpenAPI spec ${{ env.UPDATE_REASON }}"
title: "docs: OpenAPI spec ${{ env.UPDATE_REASON }}"
body: |
This PR updates the OpenAPI spec.
Update reason: ${{ env.UPDATE_REASON }}
Version: ${{ env.NEW_VERSION }}
branch: update-openapi-spec
branch-suffix: timestamp
delete-branch: true
reviewers: mendonk
docs_test .github/workflows/docs_test.yml
View raw YAML
name: Test Docs Build
on:
workflow_call:
workflow_dispatch:
inputs:
branch:
description: "(Optional) Branch to checkout"
required: false
type: string
env:
NODE_VERSION: "22"
jobs:
test-docs-build:
name: Test Docs Build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
id: setup-node
with:
node-version: ${{ env.NODE_VERSION }}
cache: npm
cache-dependency-path: ./docs/package-lock.json
- name: Install dependencies
run: cd docs && npm install
- name: Build docs
run: cd docs && npm run build
integration_tests matrix .github/workflows/integration_tests.yml
View raw YAML
name: Integration Tests
on:
workflow_dispatch:
inputs:
ref:
description: "(Optional) ref to checkout"
required: false
type: string
workflow_call:
inputs:
python-versions:
description: "(Optional) Python versions to test"
required: true
type: string
default: "['3.10', '3.11', '3.12', '3.13']"
ref:
description: "(Optional) ref to checkout"
required: false
type: string
env:
POETRY_VERSION: "1.8.2"
jobs:
integration-tests:
name: Run Integration Tests
runs-on: ubuntu-latest
strategy:
max-parallel: 1 # Currently, we can only run at a time for collection-per-db-constraints
matrix:
python-version:
- "3.13"
- "3.12"
- "3.11"
- "3.10"
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ASTRA_DB_API_ENDPOINT: ${{ secrets.ASTRA_DB_API_ENDPOINT }}
ASTRA_DB_APPLICATION_TOKEN: ${{ secrets.ASTRA_DB_APPLICATION_TOKEN }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: "Setup Environment"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Run integration tests with api keys
timeout-minutes: 20
run: |
make integration_tests_api_keys
jest_test .github/workflows/jest_test.yml
View raw YAML
name: Run Frontend Jest Unit Tests
on:
workflow_call:
secrets:
CODECOV_TOKEN:
required: false
inputs:
ref:
description: "(Optional) ref to checkout"
required: false
type: string
workflow_dispatch:
inputs:
ref:
description: "(Optional) ref to checkout"
required: false
type: string
env:
NODE_VERSION: "22"
jobs:
jest-unit-tests:
name: Frontend Jest Unit Tests
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
checks: write
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node.js Environment
uses: actions/setup-node@v6
id: setup-node
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
cache-dependency-path: ./src/frontend/package-lock.json
- name: Run Frontend Unit Tests
run: make test_frontend_ci
- name: Publish Test Results
uses: mikepenz/action-junit-report@v5
if: always()
with:
report_paths: 'src/frontend/test-results/junit.xml'
check_name: 'Frontend Unit Test Results'
fail_on_failure: true
require_tests: true
- name: Add Coverage PR Comment
uses: MishaKav/jest-coverage-comment@main
if: github.event_name == 'pull_request'
with:
coverage-summary-path: src/frontend/coverage/coverage-summary.json
title: Frontend Unit Test Coverage Report
summary-title: Coverage Summary
badge-title: Coverage
hide-comment: false
create-new-comment: false
hide-summary: false
junitxml-title: Unit Test Results
junitxml-path: src/frontend/test-results/junit.xml
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: src/frontend/coverage/lcov.info
flags: frontend
name: frontend-coverage
fail_ci_if_error: false
directory: src/frontend/coverage/
- name: Upload Coverage Reports
uses: actions/upload-artifact@v6
if: always()
with:
name: frontend-coverage-report
path: src/frontend/coverage/
retention-days: 30js_autofix perms .github/workflows/js_autofix.yml
View raw YAML
name: autofix.ci
on:
pull_request:
paths:
- "src/frontend/**"
permissions:
contents: read
env:
NODE_VERSION: "22"
jobs:
autofix:
if: ${{ github.actor != 'github-actions[bot]' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Node.js
uses: actions/setup-node@v6
id: setup-node
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache Node.js dependencies
uses: actions/cache@v5
id: npm-cache
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('src/frontend/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Node.js dependencies
run: |
cd src/frontend
npm ci
if: ${{ steps.setup-node.outputs.cache-hit != 'true' }}
- name: Run Biome
run: |
cd src/frontend
npm run format
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
lint-js .github/workflows/lint-js.yml
View raw YAML
name: Lint Frontend
on:
workflow_call:
workflow_dispatch:
inputs:
branch:
description: "(Optional) Branch to checkout"
required: false
type: string
env:
NODE_VERSION: "22"
jobs:
run-linters:
name: Run Biome
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
id: setup-node
with:
node-version: ${{ env.NODE_VERSION }}
- name: Cache Node.js dependencies
uses: actions/cache@v5
id: npm-cache
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('src/frontend/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
- name: Install Node.js dependencies
run: |
cd src/frontend
npm install
if: ${{ steps.setup-node.outputs.cache-hit != 'true' }}
- name: Run Biome
run: |
cd src/frontend
npx @biomejs/biome check --changed
lint-py matrix .github/workflows/lint-py.yml
View raw YAML
name: Lint Python
on:
workflow_call:
workflow_dispatch:
inputs:
branch:
description: "(Optional) Branch to checkout"
required: false
type: string
env:
POETRY_VERSION: "1.8.2"
jobs:
lint:
name: Run Mypy
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.13"
- "3.12"
- "3.11"
- "3.10"
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.branch || github.ref }}
persist-credentials: true
- name: "Setup Environment"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Install the project
run: uv sync
- name: Run Mypy
run: |
uv run mypy --namespace-packages -p "langflow"
env:
GITHUB_TOKEN: ${{ secrets.github_token }}
migration-validation .github/workflows/migration-validation.yml
View raw YAML
name: Database Migration Validation
on:
pull_request:
paths:
- 'src/backend/base/langflow/alembic/versions/*.py'
- 'alembic/versions/*.py'
jobs:
validate-migration:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install sqlalchemy alembic
- name: Get changed migration files
id: changed-files
run: |
# Get all changed Python files in alembic/versions directories
# CHANGED_FILES=$(git diff --name-only origin/main...HEAD | grep -E '(alembic|migrations)/versions/.*\.py$' || echo "")
# Exclude test migrations, as they are not part of the main codebase
CHANGED_FILES=$(git diff --name-only origin/main...HEAD | grep -E '(alembic|migrations)/versions/.*\.py$' | grep -v 'test_migrations/' || echo "")
if [ -z "$CHANGED_FILES" ]; then
echo "No migration files changed"
echo "files=" >> $GITHUB_OUTPUT
else
echo "Changed migration files:"
echo "$CHANGED_FILES"
# Convert newlines to spaces for passing as arguments
echo "files=$(echo $CHANGED_FILES | tr '\n' ' ')" >> $GITHUB_OUTPUT
fi
- name: Validate migrations
if: steps.changed-files.outputs.files != ''
run: |
python src/backend/base/langflow/alembic/migration_validator.py ${{ steps.changed-files.outputs.files }}
# - name: Check migration phase sequence
# if: steps.changed-files.outputs.files != ''
# run: |
# python scripts/check_phase_sequence.py ${{ steps.changed-files.outputs.files }}
- name: Generate validation report
if: always() && steps.changed-files.outputs.files != ''
run: |
python src/backend/base/langflow/alembic/migration_validator.py \
--json ${{ steps.changed-files.outputs.files }} > validation-report.json || true
- name: Post PR comment with results
if: always() && steps.changed-files.outputs.files != ''
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
let message = '';
let validationPassed = true;
try {
const report = JSON.parse(fs.readFileSync('validation-report.json', 'utf8'));
for (const result of report) {
if (!result.valid) {
validationPassed = false;
}
}
if (validationPassed) {
message = `✅ **Migration Validation Passed**\n\n`;
message += `All migrations follow the Expand-Contract pattern correctly.\n\n`;
} else {
message = `❌ **Migration Validation Failed**\n\n`;
message += `Your migrations don't follow the Expand-Contract pattern.\n\n`;
for (const result of report) {
if (!result.valid || result.warnings.length > 0) {
message += `### File: \`${result.file.split('/').pop()}\`\n`;
message += `**Phase:** ${result.phase}\n\n`;
if (result.violations && result.violations.length > 0) {
message += `**Violations:**\n`;
for (const v of result.violations) {
message += `- Line ${v.line}: ${v.message}\n`;
}
message += `\n`;
}
if (result.warnings && result.warnings.length > 0) {
message += `**Warnings:**\n`;
for (const w of result.warnings) {
message += `- Line ${w.line}: ${w.message}\n`;
}
message += `\n`;
}
}
}
message += `### 📚 Resources\n`;
message += `- Review the [DB Migration Guide](./src/backend/base/langflow/alembic/DB-MIGRATION-GUIDE.MD)\n`;
message += `- Use \`python scripts/generate_migration.py --help\` to generate compliant migrations\n\n`;
message += `### Common Issues & Solutions\n`;
message += `- **New columns must be nullable:** Add \`nullable=True\` or \`server_default\`\n`;
message += `- **Missing phase marker:** Add \`Phase: EXPAND/MIGRATE/CONTRACT\` to docstring\n`;
message += `- **Column drops:** Only allowed in CONTRACT phase\n`;
message += `- **Direct renames:** Use expand-contract pattern instead\n`;
}
} catch (error) {
message = `⚠️ **Migration validation check failed to run properly**\n`;
message += `Error: ${error.message}\n`;
}
// Post or update comment
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(comment =>
comment.user.type === 'Bot' &&
comment.body.includes('Migration Validation')
);
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body: message
});
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: message
});
}
// Fail the workflow if validation didn't pass
if (!validationPassed) {
core.setFailed('Migration validation failed');
}nightly_build .github/workflows/nightly_build.yml
View raw YAML
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_release_tag.outputs.release_tag }}
base_tag: ${{ steps.set_base_tag.outputs.base_tag }}
lfx_tag: ${{ steps.generate_lfx_tag.outputs.lfx_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 main nightly tag
id: generate_release_tag
run: |
# NOTE: This outputs the tag with the `v` prefix.
RELEASE_TAG="$(uv run ./scripts/ci/pypi_nightly_tag.py main)"
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
echo "release_tag=$RELEASE_TAG"
- name: Delete existing tag if it exists
id: check_release_tag
run: |
git fetch --tags
git tag -d ${{ steps.generate_release_tag.outputs.release_tag }} || true
git push --delete origin ${{ steps.generate_release_tag.outputs.release_tag }} || true
echo "release_tag_exists=false" >> $GITHUB_OUTPUT
- name: Generate base nightly tag
id: generate_base_tag
run: |
# NOTE: This outputs the tag with the `v` prefix.
BASE_TAG="$(uv run ./scripts/ci/pypi_nightly_tag.py base)"
echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
echo "base_tag=$BASE_TAG"
- 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: 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_release_tag.outputs.release_tag }}"
BASE_TAG="${{ steps.generate_base_tag.outputs.base_tag }}"
LFX_TAG="${{ steps.generate_lfx_tag.outputs.lfx_tag }}"
echo "Updating LFX project version to $LFX_TAG"
uv run ./scripts/ci/update_lfx_version.py $LFX_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 uv.lock src/backend/base/uv.lock
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_release_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_base_tag.outputs.base_tag }}" ]; then
BASE_TAG="${{ steps.generate_base_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"]'
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.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 }}
# 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
slack-notification:
name: Send Slack Notification
needs: [frontend-tests-linux, frontend-tests-windows, backend-unit-tests, release-nightly-build]
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.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' }}
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"
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' }}
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 }}
py_autofix .github/workflows/py_autofix.yml
View raw YAML
name: autofix.ci
on:
pull_request:
paths:
- "**/*.py"
- "src/lfx/src/lfx/components/**"
- "scripts/build_component_index.py"
env:
PYTHON_VERSION: "3.13"
jobs:
lint:
name: Run Ruff Check and Format
if: ${{ github.actor != 'github-actions[bot]' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- 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
- run: uv run ruff check --fix-only .
- run: uv run ruff format . --config pyproject.toml
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
update-starter-projects:
name: Update Starter Projects
if: ${{ github.actor != 'github-actions[bot]' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- 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 dependencies"
run: |
uv sync
uv pip install -e .
- name: Run starter projects update
run: uv run python scripts/ci/update_starter_projects.py
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
update-component-index:
name: Update Component Index
if: ${{ github.actor != 'github-actions[bot]' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- 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 dependencies"
run: uv sync --dev --project .
- name: Build component index
env:
LFX_DEV: "1"
run: make build_component_index
- uses: autofix-ci/action@635ffb0c9798bd160680f18fd73371e355b85f27
python_test matrix .github/workflows/python_test.yml
View raw YAML
name: Python tests
on:
workflow_call:
secrets:
OPENAI_API_KEY:
required: true
ANTHROPIC_API_KEY:
required: true
CODECOV_TOKEN:
required: false
inputs:
python-versions:
description: "(Optional) Python versions to test"
required: true
type: string
default: "['3.10', '3.11', '3.12', '3.13']"
ref:
description: "(Optional) ref to checkout"
required: false
type: string
nightly:
description: "Whether run is from the nightly build"
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:
python-versions:
description: "(Optional) Python versions to test"
required: true
type: string
default: "['3.10', '3.11', '3.12', '3.13']"
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
env:
POETRY_VERSION: "1.8.2"
NODE_VERSION: "22"
PYTEST_RUN_PATH: "src/backend/tests"
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
jobs:
build:
name: Unit Tests - Python ${{ matrix.python-version }} - Group ${{ matrix.group }}
runs-on: ${{ (inputs['runs-on'] && startsWith(format('{0}', inputs['runs-on']), '[') && fromJSON(inputs['runs-on'])) || inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
strategy:
matrix:
python-version: ${{ fromJson(inputs.python-versions || '["3.10", "3.11", "3.12", "3.13"]' ) }}
splitCount: [5]
group: [1, 2, 3, 4, 5]
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
id: setup-node
with:
node-version: ${{ env.NODE_VERSION }}
- name: "Setup Environment"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Install the project
run: uv sync
- name: Generate dynamic coverage configuration
run: |
echo "Generating dynamic coverage configuration..."
python3 scripts/generate_coverage_config.py
echo "Generated .coveragerc with the following exclusions:"
echo "Bundled components and legacy files excluded from coverage"
- name: Run unit tests
uses: nick-fields/retry@v3
with:
timeout_minutes: 12
max_attempts: 2
command: make unit_tests args="-x -vv --splits ${{ matrix.splitCount }} --group ${{ matrix.group }} --reruns 5 --cov --cov-config=src/backend/.coveragerc --cov-report=xml --cov-report=html"
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
if: matrix.python-version == '3.10'
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./coverage.xml
flags: backend
name: backend-coverage-group-${{ matrix.group }}
fail_ci_if_error: false
directory: ./
- name: Upload coverage artifacts
uses: actions/upload-artifact@v6
if: matrix.python-version == '3.10'
with:
name: backend-coverage-report-group-${{ matrix.group }}
path: |
coverage.xml
htmlcov/
retention-days: 30
integration-tests:
name: Integration Tests - Python ${{ matrix.python-version }}
runs-on: ${{ (inputs['runs-on'] && startsWith(format('{0}', inputs['runs-on']), '[') && fromJSON(inputs['runs-on'])) || inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
strategy:
matrix:
python-version: ${{ fromJson(inputs.python-versions || '["3.10", "3.11", "3.12", "3.13"]' ) }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: "Setup Environment"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Install the project
run: uv sync
- name: Run integration tests
run: make integration_tests_no_api_keys
env:
PYLEAK_LOG_LEVEL: debug # enable pyleak logging
DO_NOT_TRACK: true # disable telemetry reporting
lfx-tests:
name: LFX Tests - Python ${{ matrix.python-version }}
runs-on: ${{ (inputs['runs-on'] && startsWith(format('{0}', inputs['runs-on']), '[') && fromJSON(inputs['runs-on'])) || inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
strategy:
matrix:
python-version: ${{ fromJson(inputs.python-versions || '["3.10", "3.11", "3.12", "3.13"]' ) }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: "Setup Environment"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Run lfx tests
run: uv run make lfx_tests
env:
DO_NOT_TRACK: true # disable telemetry reporting
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
if: matrix.python-version == '3.10'
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ./src/lfx/coverage.xml
flags: lfx
name: lfx-coverage
fail_ci_if_error: false
directory: ./src/lfx/
- name: Upload coverage artifacts
uses: actions/upload-artifact@v6
if: matrix.python-version == '3.10'
with:
name: lfx-coverage-report
path: |
src/lfx/coverage.xml
src/lfx/htmlcov/
retention-days: 30
test-cli:
name: Test CLI - Python ${{ matrix.python-version }}
runs-on: ${{ (inputs['runs-on'] && startsWith(format('{0}', inputs['runs-on']), '[') && fromJSON(inputs['runs-on'])) || inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
strategy:
matrix:
python-version: ${{ fromJson(inputs.python-versions || '["3.10", "3.11", "3.12", "3.13"]') }}
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: "Setup Environment"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Check Version
id: check-version
# We need to print $3 because langflow-base is a dependency of langflow
# For langlow we'd use print $2
run: |
version=$(uv tree | grep 'langflow-base' | awk '{print $3}' | sed 's/^v//' | head -n 1)
url="https://pypi.org/pypi/langflow-base/json"
if [ ${{ inputs.nightly }} == true ]; then
url="https://pypi.org/pypi/langflow-base-nightly/json"
fi
last_released_version=$(curl -s $url | jq -r '.releases | keys | .[]' | sort -V | tail -n 1)
if [ "$version" != "$last_released_version" ]; then
echo "Version $version has not been released yet. Skipping the rest of the job."
echo skipped=true >> $GITHUB_OUTPUT
exit 0
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
- name: Build wheel
if: steps.check-version.outputs.skipped == 'false'
run: |
make build main=true
- name: Install wheel and Test CLI
if: steps.check-version.outputs.skipped == 'false'
run: |
uv venv new-venv
source new-venv/bin/activate
uv pip install dist/*.whl
- name: Test CLI
if: steps.check-version.outputs.skipped == 'false'
run: |
source new-venv/bin/activate
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 5; 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
release .github/workflows/release.yml
View raw YAML
name: Langflow Release
run-name: Langflow Release by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
release_tag:
description: "Tag to release from. This is the tag that contains the source code for the release."
required: true
type: string
release_package_base:
description: "Release Langflow Base"
required: true
type: boolean
default: false
release_package_main:
description: "Release Langflow"
required: true
type: boolean
default: false
release_lfx:
description: "Release LFX package (manually triggered)"
required: false
type: boolean
default: false
build_docker_base:
description: "Build Docker Image for Langflow Base"
required: true
type: boolean
default: false
build_docker_main:
description: "Build Docker Image for Langflow"
required: true
type: boolean
default: false
pre_release:
description: "Pre-release"
required: false
type: boolean
default: false
create_release:
description: "Whether to create a gh release"
required: false
type: boolean
default: false
dry_run:
description: "Dry run mode - disables all pushes to external services (PyPI, Docker, GitHub releases)"
required: false
type: boolean
default: true
jobs:
echo-inputs:
name: Echo Workflow Inputs
runs-on: ubuntu-latest
steps:
- name: Echo workflow inputs
run: |
echo "release_tag: ${{ inputs.release_tag }}"
echo "release_package_base: ${{ inputs.release_package_base }}"
echo "release_package_main: ${{ inputs.release_package_main }}"
echo "release_lfx: ${{ inputs.release_lfx }}"
echo "build_docker_base: ${{ inputs.build_docker_base }}"
echo "build_docker_main: ${{ inputs.build_docker_main }}"
echo "pre_release: ${{ inputs.pre_release }}"
echo "create_release: ${{ inputs.create_release }}"
echo "dry_run: ${{ inputs.dry_run }}"
validate-tag:
name: Validate Tag Input
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
fetch-depth: 0 # Fetch all history - required for tags (?)
- name: Validate that input is a tag, not a branch
run: |
# Check if the input exists as a tag
if ! git tag -l | grep -q "^${{ inputs.release_tag }}$"; then
echo "Error: '${{ inputs.release_tag }}' is not a valid tag."
echo "Available tags:"
git tag -l | head -20
exit 1
fi
# Check if the input also exists as a branch (warn if so, but don't fail)
if git branch -r | grep -q "origin/${{ inputs.release_tag }}$"; then
echo "Tag '${{ inputs.release_tag }}' also exists as a branch. Exiting out of caution."
exit 1
fi
echo "Validated: '${{ inputs.release_tag }}' is a valid tag."
validate-dependencies:
name: Validate Release Dependencies
runs-on: ubuntu-latest
if: ${{ inputs.release_package_base || inputs.release_package_main || inputs.release_lfx || inputs.build_docker_base || inputs.build_docker_main }}
needs: [validate-tag]
steps:
- name: Validate that build-base is enabled if build-main is enabled
run: |
if [ "${{ inputs.release_package_main }}" = "true" ] && [ "${{ inputs.release_package_base }}" = "false" ]; then
echo "Error: Cannot release Langflow Main without releasing Langflow Base."
echo "Please enable 'release_package_base' or disable 'release_package_main'."
exit 1
fi
echo "✅ Release dependencies validated successfully."
determine-base-version:
name: Determine Base Version
needs: [validate-tag, validate-dependencies]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Install the project
run: uv sync
- name: Determine version
id: version
run: |
version=$(python3 -c "
import tomllib, pathlib
data = tomllib.loads(pathlib.Path('src/backend/base/pyproject.toml').read_text())
print(data['project']['version'])
")
echo "Base version from pyproject.toml: $version"
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://pypi.org/pypi/langflow-base/json" | jq -r '.releases | keys | .[]' | grep -E '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")"
echo "Latest base pre-release version: $last_released_version"
echo "Base pre-release version to be released: $version"
else
last_released_version=$(curl -s "https://pypi.org/pypi/langflow-base/json" | jq -r '.releases | keys | .[]' | grep -vE '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
echo "Latest base release version: $last_released_version"
fi
if [ "$version" = "$last_released_version" ]; then
echo "Base pypi version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 1
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
determine-main-version:
name: Determine Main Version
needs: [validate-tag, validate-dependencies]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Install the project
run: uv sync
- name: Determine version
id: version
run: |
version=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | awk '{print $2}' | sed 's/^v//')
echo "Main version from pyproject.toml: $version"
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://pypi.org/pypi/langflow/json" | jq -r '.releases | keys | .[]' | grep -E '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")"
echo "Latest main pre-release version: $last_released_version"
echo "Main pre-release version to be released: $version"
else
last_released_version=$(curl -s "https://pypi.org/pypi/langflow/json" | jq -r '.releases | keys | .[]' | grep -vE '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
echo "Latest main release version: $last_released_version"
fi
if [ "$version" = "$last_released_version" ]; then
echo "Main pypi version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 1
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
determine-lfx-version:
name: Determine LFX Version
needs: [validate-tag, validate-dependencies]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
skipped: ${{ steps.version.outputs.skipped }}
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Install LFX dependencies
run: uv sync --dev --package lfx
- name: Determine version
id: version
run: |
version=$(uv tree | grep 'lfx' | awk '{print $3}' | sed 's/^v//' | head -n 2 | xargs)
echo "LFX version from pyproject.toml: $version"
if [ ${{inputs.pre_release}} == "true" ]; then
last_released_version=$(curl -s "https://pypi.org/pypi/lfx/json" | jq -r '.releases | keys | .[]' | grep -E '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
version="$(uv run ./scripts/ci/langflow_pre_release_tag.py "$version" "$last_released_version")"
echo "Latest LFX pre-release version: $last_released_version"
echo "LFX pre-release version to be released: $version"
else
last_released_version=$(curl -s "https://pypi.org/pypi/lfx/json" | jq -r '.releases | keys | .[]' | grep -vE '(a|b|rc|dev|alpha|beta)' | sort -V | tail -n 1)
echo "Latest LFX release version: $last_released_version"
fi
if [ "$version" = "$last_released_version" ]; then
echo "LFX pypi version $version is already released. Skipping release."
echo skipped=true >> $GITHUB_OUTPUT
exit 1
else
echo version=$version >> $GITHUB_OUTPUT
echo skipped=false >> $GITHUB_OUTPUT
fi
ci:
name: CI
needs: [validate-tag, validate-dependencies]
uses: ./.github/workflows/ci.yml
with:
ref: ${{ inputs.release_tag }}
python-versions: "['3.10', '3.11', '3.12', '3.13']"
frontend-tests-folder: "tests"
release: true
run-all-tests: true
runs-on: ubuntu-latest
secrets: inherit
build-lfx:
name: Build LFX
needs: [determine-lfx-version]
if: ${{ needs.determine-lfx-version.outputs.skipped == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.13"
prune-cache: false
- name: Install LFX dependencies
run: uv sync --dev --package lfx
- name: Set version for pre-release
if: ${{ inputs.pre_release }}
run: |
VERSION="${{ needs.determine-lfx-version.outputs.version }}"
echo "Setting pre-release version to: $VERSION"
cd src/lfx
# Update version in lfx pyproject.toml
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
# Verify the change
echo "Updated pyproject.toml version:"
grep "^version" pyproject.toml
- name: Build project for distribution
run: |
cd src/lfx
rm -rf dist/
uv build --wheel --out-dir dist
- name: Verify built version
run: |
EXPECTED_VERSION="${{ needs.determine-lfx-version.outputs.version }}"
WHEEL_FILE=$(ls src/lfx/dist/*.whl)
echo "Built wheel: $WHEEL_FILE"
NORMALIZED_VERSION=$(echo "$EXPECTED_VERSION" | sed 's/\.rc/rc/g; s/\.a/a/g; s/\.b/b/g; s/\.dev/dev/g')
echo "Expected version: $EXPECTED_VERSION"
echo "Normalized for wheel: $NORMALIZED_VERSION"
if [[ ! "$WHEEL_FILE" =~ $NORMALIZED_VERSION ]]; then
echo "❌ Error: Wheel version doesn't match expected version"
echo "Expected: $EXPECTED_VERSION (normalized: $NORMALIZED_VERSION)"
echo "Wheel file: $WHEEL_FILE"
exit 1
fi
echo "✅ Version verified: $EXPECTED_VERSION"
- name: Test CLI
run: |
cd src/lfx
uv pip install dist/*.whl --force-reinstall
uv run lfx --help
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: dist-lfx
path: src/lfx/dist
build-base:
name: Build Langflow Base
needs: [build-lfx, determine-base-version, determine-lfx-version]
if: ${{ inputs.release_package_base && needs.determine-base-version.outputs.skipped == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.12"
prune-cache: false
- name: Download LFX artifact
uses: actions/download-artifact@v7
with:
name: dist-lfx
path: ./lfx-dist
- name: Install dependencies with local LFX wheel
run: |
# Create virtual environment
uv venv
# Install using pip with local wheel directory as find-links
uv pip install --find-links ./lfx-dist --prerelease=allow -e src/backend/base
- name: Check for dependency incompatibility
run: uv pip check
- name: Set version for pre-release
if: ${{ inputs.pre_release }}
run: |
VERSION="${{ needs.determine-base-version.outputs.version }}"
echo "Setting pre-release version to: $VERSION"
cd src/backend/base
# Update version in pyproject.toml
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
# Verify the change
echo "Updated pyproject.toml version:"
grep "^version" pyproject.toml
- name: Update lfx dependency for pre-release
if: ${{ inputs.pre_release }}
run: |
LFX_VERSION="${{ needs.determine-lfx-version.outputs.version }}"
echo "Updating lfx dependency to allow pre-release version: $LFX_VERSION"
cd src/backend/base
# Extract current lfx constraint from pyproject.toml
CURRENT_CONSTRAINT=$(grep -E '^\s*"lfx' pyproject.toml | head -1)
echo "Current constraint: $CURRENT_CONSTRAINT"
# Extract the major.minor version (e.g., "0.3" from "~=0.3.0")
MAJOR_MINOR=$(echo "$CURRENT_CONSTRAINT" | sed -E 's/.*[~>=<]+([0-9]+\.[0-9]+).*/\1/')
NEXT_MAJOR=$((${MAJOR_MINOR%.*} + 1))
# Create new constraint: >=LFX_VERSION,<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,
determine-base-version,
determine-main-version,
determine-lfx-version,
]
if: ${{ inputs.release_package_main && needs.determine-main-version.outputs.skipped == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.release_tag }}
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: "3.12"
prune-cache: false
- name: Download LFX artifact
uses: actions/download-artifact@v7
with:
name: dist-lfx
path: ./lfx-dist
- name: Download base artifact
uses: actions/download-artifact@v7
with:
name: dist-base
path: ./base-dist
- name: Install dependencies with local wheels
run: |
# Create virtual environment
uv venv
# Install using pip with local wheel directories as find-links
uv pip install --find-links ./lfx-dist --find-links ./base-dist --prerelease=allow -e .
- name: Check for dependency incompatibility
run: uv pip check
- name: Set version for pre-release
if: ${{ inputs.pre_release }}
run: |
VERSION="${{ needs.determine-main-version.outputs.version }}"
echo "Setting main pre-release version to: $VERSION"
# Update version in main pyproject.toml
sed -i.bak "s/^version = .*/version = \"$VERSION\"/" pyproject.toml
# Verify the change
echo "Updated pyproject.toml version:"
grep "^version" pyproject.toml
- name: Update langflow-base dependency for pre-release
if: ${{ inputs.pre_release }}
run: |
BASE_VERSION="${{ needs.determine-base-version.outputs.version }}"
echo "Base version for pre-release: $BASE_VERSION"
# Extract current langflow-base constraint from pyproject.toml
CURRENT_CONSTRAINT=$(grep -E '^\s*"langflow-base' pyproject.toml | head -1)
echo "Current constraint: $CURRENT_CONSTRAINT"
# Extract the major.minor version (e.g., "0.8" from "~=0.8.0")
MAJOR_MINOR=$(echo "$CURRENT_CONSTRAINT" | sed -E 's/.*[~>=<]+([0-9]+\.[0-9]+).*/\1/')
NEXT_MAJOR=$((${MAJOR_MINOR%.*} + 1))
# Create new constraint: >=BASE_VERSION,<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
test-cross-platform:
name: Test Cross-Platform Installation
needs: [build-base, build-main, build-lfx]
if: |
always() &&
!cancelled() &&
(needs.build-base.result == 'success' || needs.build-main.result == 'success' || needs.build-lfx.result == 'success')
uses: ./.github/workflows/cross-platform-test.yml
with:
base-artifact-name: "dist-base"
main-artifact-name: "dist-main"
lfx-artifact-name: "dist-lfx"
pre_release: ${{ inputs.pre_release }}
publish-base:
name: Publish Langflow Base to PyPI
if: ${{ inputs.release_package_base }}
needs: [build-base, test-cross-platform, ci]
runs-on: ubuntu-latest
steps:
- name: Download base artifact
uses: actions/download-artifact@v7
with:
name: dist-base
path: src/backend/base/dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: "3.13"
- name: Publish base to PyPI
if: ${{ !inputs.dry_run }}
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
cd src/backend/base && uv publish dist/*.whl
publish-main:
name: Publish Langflow Main to PyPI
if: ${{ inputs.release_package_main }}
needs: [build-main, test-cross-platform, publish-base, ci]
runs-on: ubuntu-latest
steps:
- name: Download main artifact
uses: actions/download-artifact@v7
with:
name: dist-main
path: dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: "3.13"
- name: Publish main to PyPI
if: ${{ !inputs.dry_run }}
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
uv publish dist/*.whl
publish-lfx:
name: Publish LFX to PyPI
if: ${{ inputs.release_lfx }}
needs: [build-lfx, test-cross-platform, ci]
runs-on: ubuntu-latest
steps:
- name: Download LFX artifact
uses: actions/download-artifact@v7
with:
name: dist-lfx
path: src/lfx/dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: "3.13"
- name: Publish LFX to PyPI
if: ${{ !inputs.dry_run }}
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
cd src/lfx && uv publish dist/*.whl
call_docker_build_base:
name: Call Docker Build Workflow for Langflow Base
if: ${{ inputs.build_docker_base }}
needs: [ci]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: base
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main:
name: Call Docker Build Workflow for Langflow
if: ${{ inputs.build_docker_main }}
needs: [ci]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main_backend:
name: Call Docker Build Workflow for Langflow Backend
if: ${{ inputs.build_docker_main && !inputs.dry_run }}
needs: [call_docker_build_main]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main-backend
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main_frontend:
name: Call Docker Build Workflow for Langflow Frontend
if: ${{ inputs.build_docker_main && !inputs.dry_run }}
needs: [call_docker_build_main]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main-frontend
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main_ep:
name: Call Docker Build Workflow for Langflow with Entrypoint
if: ${{ inputs.build_docker_main }}
needs: [ci]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main-ep
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
call_docker_build_main_all:
name: Call Docker Build Workflow for langflow-all
if: ${{ inputs.build_docker_main }}
needs: [ci]
uses: ./.github/workflows/docker-build-v2.yml
with:
ref: ${{ inputs.release_tag }}
release_type: main-all
pre_release: ${{ inputs.pre_release }}
push_to_registry: ${{ !inputs.dry_run }}
secrets: inherit
create_release:
name: Create Release
runs-on: ubuntu-latest
needs: [determine-main-version, build-main, publish-main]
if: |
always() &&
!cancelled() &&
!inputs.dry_run &&
inputs.create_release &&
needs.build-main.result == 'success' &&
needs.publish-main.result == 'success'
steps:
- uses: actions/download-artifact@v4
with:
name: dist-main
path: dist
- name: Create Release
uses: ncipollo/release-action@v1
with:
artifacts: dist/*
token: ${{ secrets.GITHUB_TOKEN }}
draft: false
generateReleaseNotes: true
prerelease: ${{ inputs.pre_release }}
tag: ${{ needs.determine-main-version.outputs.version }}
allowUpdates: true
updateOnlyUnreleased: false
release-lfx matrix perms .github/workflows/release-lfx.yml
View raw YAML
name: LFX Release
run-name: LFX Release ${{ github.event.inputs.version || 'dev' }} by @${{ github.actor }}
on:
workflow_dispatch:
inputs:
version:
description: "Version to release (e.g., 0.1.0)"
required: true
type: string
publish_pypi:
description: "Publish to PyPI"
required: true
type: boolean
default: true
build_docker:
description: "Build and publish Docker images"
required: true
type: boolean
default: true
pre_release:
description: "Mark as pre-release"
required: false
type: boolean
default: false
create_github_release:
description: "Create GitHub release"
required: true
type: boolean
default: true
env:
PYTHON_VERSION: "3.13"
permissions:
contents: write
packages: write
jobs:
validate-version:
name: Validate Version
runs-on: ubuntu-latest
outputs:
should_release: ${{ steps.check.outputs.should_release }}
current_version: ${{ steps.check.outputs.current_version }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- 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: Check version
id: check
run: |
cd src/lfx
# Use uv tree to get package info, consistent with nightly workflow
name=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $1}')
version=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $2}')
# Strip leading 'v' if present
version=$(echo $version | sed 's/^v//')
echo "current_version=$version" >> $GITHUB_OUTPUT
if [ "$version" != "${{ github.event.inputs.version }}" ]; then
echo "❌ Version mismatch: package has $version but input is ${{ github.event.inputs.version }}"
echo "Please update the version in pyproject.toml first"
echo "should_release=false" >> $GITHUB_OUTPUT
exit 1
fi
# Check if version already exists on PyPI
if curl -s "https://pypi.org/pypi/lfx/json" | jq -r '.releases | keys[]' | grep -q "^${{ github.event.inputs.version }}$"; then
echo "❌ Version ${{ github.event.inputs.version }} already exists on PyPI"
echo "should_release=false" >> $GITHUB_OUTPUT
exit 1
fi
echo "✅ Version ${{ github.event.inputs.version }} is valid and not yet released"
echo "should_release=true" >> $GITHUB_OUTPUT
run-tests:
name: Run Tests
needs: validate-version
if: needs.validate-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.10", "3.11", "3.12", "3.13"]
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Run LFX tests
run: |
cd src/lfx
make test
- name: Test CLI installation
run: |
cd src/lfx
uv pip install .
uv run lfx --help
uv run lfx run --help
uv run lfx serve --help
release-lfx:
name: Build and Release LFX
needs: [validate-version, run-tests]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.check-version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v6
- 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 LFX dependencies
run: uv sync --dev --package lfx
- name: Verify Version
id: check-version
run: |
cd src/lfx
# Use uv tree to get package info, consistent with nightly workflow
name=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $1}')
version=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $2}')
# Verify package name
if [ "$name" != "lfx" ]; then
echo "Package name $name does not match lfx. Exiting the workflow."
exit 1
fi
# Strip leading 'v' if present
version=$(echo $version | sed 's/^v//')
# Verify version matches input
if [ "$version" != "${{ github.event.inputs.version }}" ]; then
echo "Version $version does not match input ${{ github.event.inputs.version }}. Exiting the workflow."
exit 1
fi
echo "version=$version" >> $GITHUB_OUTPUT
- name: Build distribution
run: |
cd src/lfx
rm -rf dist/
uv build --wheel --out-dir dist
- name: Check build artifacts
run: |
cd src/lfx
ls -la dist/
# Verify wheel contents
unzip -l dist/*.whl | grep -E "(lfx/__main__.py|lfx/cli/run.py|lfx/cli/commands.py)"
- name: Test installation from wheel
run: |
cd src/lfx
uv pip install dist/*.whl --force-reinstall
uv run lfx --help
echo "LFX CLI test completed successfully"
- name: Upload artifacts
uses: actions/upload-artifact@v6
with:
name: lfx-dist
path: src/lfx/dist/
retention-days: 5
- name: Publish to PyPI
if: github.event.inputs.publish_pypi == 'true'
env:
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
cd src/lfx
uv publish dist/*.whl
build-docker:
name: Build Docker Images
needs: [validate-version, run-tests]
if: github.event.inputs.build_docker == 'true'
runs-on: ubuntu-latest
strategy:
matrix:
variant: [production, alpine]
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Docker System Info and Cleanup
run: |
echo "=== Docker System Usage Before Cleanup ==="
docker system df || true
docker buildx du || true
echo "=== Cleaning up Docker System ==="
docker system prune -af --volumes || true
docker buildx prune -af || true
echo "=== Docker System Usage After Cleanup ==="
docker system df || true
docker buildx du || true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: |
langflowai/lfx
ghcr.io/langflow-ai/lfx
tags: |
type=raw,value=${{ github.event.inputs.version }}${{ matrix.variant == 'alpine' && '-alpine' || '' }}
type=raw,value=latest${{ matrix.variant == 'alpine' && '-alpine' || '' }},enable=${{ github.event.inputs.pre_release == 'false' }}
labels: |
org.opencontainers.image.title=LFX
org.opencontainers.image.description=Langflow Executor - CLI tool for running Langflow AI workflows
org.opencontainers.image.vendor=Langflow
org.opencontainers.image.version=${{ github.event.inputs.version }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: src/lfx/docker/Dockerfile${{ matrix.variant == 'alpine' && '.alpine' || '' }}
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
build-args: |
LFX_VERSION=${{ github.event.inputs.version }}
create-release:
name: Create GitHub Release
needs: [release-lfx, build-docker]
if: always() && github.event.inputs.create_github_release == 'true' && needs.release-lfx.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- name: Download artifacts
uses: actions/download-artifact@v7
with:
name: lfx-dist
path: dist/
- name: Generate release notes
id: notes
run: |
cat > release_notes.md << EOF
# LFX ${{ github.event.inputs.version }}
## 🚀 Installation
### PyPI
\`\`\`bash
pip install lfx==${{ github.event.inputs.version }}
# or
uv pip install lfx==${{ github.event.inputs.version }}
# or run without installing
uvx lfx@${{ github.event.inputs.version }} --help
\`\`\`
### Docker
\`\`\`bash
# Standard image
docker pull langflowai/lfx:${{ github.event.inputs.version }}
# Alpine image (smaller)
docker pull langflowai/lfx:${{ github.event.inputs.version }}-alpine
# Run a flow
docker run --rm -v \$(pwd):/app/data langflowai/lfx:${{ github.event.inputs.version }} lfx run flow.json --input-value "Hello"
\`\`\`
## 📦 What's New
<!-- Add release notes here -->
## 📋 Checksums
\`\`\`
$(cd dist && sha256sum *)
\`\`\`
---
**Full Changelog**: https://github.com/${{ github.repository }}/compare/v${{ needs.validate-version.outputs.current_version }}...lfx-v${{ github.event.inputs.version }}
EOF
- name: Create Release
uses: softprops/action-gh-release@v2
with:
tag_name: lfx-v${{ github.event.inputs.version }}
name: LFX ${{ github.event.inputs.version }}
body_path: release_notes.md
draft: false
prerelease: ${{ github.event.inputs.pre_release }}
files: |
dist/*
generate_release_notes: true
test-release:
name: Test Release
needs: [release-lfx, build-docker]
if: always() && (needs.release-lfx.result == 'success' || needs.build-docker.result == 'success')
runs-on: ubuntu-latest
steps:
- name: Wait for PyPI propagation
if: needs.release-lfx.result == 'success'
run: sleep 60
- name: Test PyPI installation
if: needs.release-lfx.result == 'success'
run: |
# Test installation using uv
uv pip install lfx==${{ github.event.inputs.version }}
uv run lfx --help
- name: Test Docker image
if: needs.build-docker.result == 'success'
run: |
# Test standard image
docker run --rm langflowai/lfx:${{ github.event.inputs.version }} lfx --help
# Test alpine image
docker run --rm langflowai/lfx:${{ github.event.inputs.version }}-alpine lfx --help
# Test with a simple flow
cat > test_flow.json << 'EOF'
{
"nodes": [],
"edges": []
}
EOF
docker run --rm -v $(pwd):/app/data langflowai/lfx:${{ github.event.inputs.version }} \
lfx run /app/data/test_flow.json --input-value "test" || true
notify:
name: Notify Release Status
needs: [create-release, test-release]
if: always()
runs-on: ubuntu-latest
steps:
- name: Notify success
if: needs.create-release.result == 'success'
run: |
echo "✅ LFX ${{ github.event.inputs.version }} released successfully!"
echo "PyPI: https://pypi.org/project/lfx/${{ github.event.inputs.version }}/"
echo "Docker Hub: https://hub.docker.com/r/langflowai/lfx/tags"
echo "GitHub Release: https://github.com/${{ github.repository }}/releases/tag/lfx-v${{ github.event.inputs.version }}"
- name: Notify failure
if: needs.create-release.result != 'success'
run: |
echo "❌ LFX ${{ github.event.inputs.version }} release failed!"
exit 1release_nightly .github/workflows/release_nightly.yml
View raw YAML
name: Langflow Nightly Build
run-name: Langflow Nightly Release by @${{ github.actor }}
on:
workflow_call:
inputs:
build_docker_base:
description: "Build Docker Image for Langflow Nightly Base"
required: true
type: boolean
default: false
build_docker_main:
description: "Build Docker Image for Langflow Nightly"
required: true
type: boolean
default: false
build_docker_ep:
description: "Build Docker Image for Langflow Nightly with Entrypoint"
required: false
type: boolean
default: false
build_lfx:
description: "Build and release LFX package"
required: false
type: boolean
default: false
nightly_tag_release:
description: "Tag for the nightly main build"
required: true
type: string
nightly_tag_base:
description: "Tag for the nightly base build"
required: true
type: string
nightly_tag_lfx:
description: "Tag for the nightly LFX build"
required: false
type: string
push_to_registry:
description: "Whether to push images to registries. Set to false for testing builds without publishing."
required: true
type: boolean
default: false
env:
POETRY_VERSION: "1.8.3"
PYTHON_VERSION: "3.13"
jobs:
build-nightly-lfx:
name: Build LFX Nightly
if: ${{ inputs.build_lfx }}
runs-on: ubuntu-latest
outputs:
version: ${{ steps.verify.outputs.version }}
defaults:
run:
shell: bash
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.nightly_tag_release }}
persist-credentials: true
- 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 LFX dependencies
run: cd src/lfx && uv sync
- name: Verify Nightly Name and Version
id: verify
run: |
cd src/lfx
name=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $1}')
version=$(uv tree | grep 'lfx' | head -n 1 | awk '{print $2}')
if [ "$name" != "lfx-nightly" ]; then
echo "Name $name does not match lfx-nightly. Exiting the workflow."
exit 1
fi
if [ "$version" != "${{ inputs.nightly_tag_lfx }}" ]; then
echo "Version $version does not match nightly tag ${{ inputs.nightly_tag_lfx }}. Exiting the workflow."
exit 1
fi
# Strip the leading `v` from the version
version=$(echo $version | sed 's/^v//')
echo "version=$version" >> $GITHUB_OUTPUT
- name: Build LFX for distribution
run: |
cd src/lfx
rm -rf dist/
uv build --wheel --out-dir dist
- name: Test LFX CLI
run: |
cd src/lfx
uv pip install dist/*.whl --force-reinstall
uv run lfx --help
echo "LFX CLI test completed successfully"
# PyPI publishing moved to after cross-platform testing
- name: Upload LFX Artifact
uses: actions/upload-artifact@v6
with:
name: dist-nightly-lfx
path: src/lfx/dist
build-nightly-base:
name: Build Langflow Nightly Base
needs: [build-nightly-lfx]
if: ${{ always() && (needs.build-nightly-lfx.result == 'success' || inputs.build_lfx == false) }}
runs-on: ubuntu-latest
defaults:
run:
shell: bash
outputs:
version: ${{ steps.verify.outputs.version }}
skipped: ${{ steps.verify.outputs.skipped }}
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.nightly_tag_release }}
persist-credentials: true
- 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: Download LFX artifact
if: inputs.build_lfx
uses: actions/download-artifact@v7
with:
name: dist-nightly-lfx
path: lfx-dist
- name: Install the project
run: uv sync
- name: Force reinstall LFX from built wheel
if: inputs.build_lfx
run: |
echo "Installing LFX from built wheel to test the actual distribution package..."
# While workspace resolution installs from source, we want to test the exact
# wheel that will be published to PyPI to catch any packaging issues
uv pip install --force-reinstall --no-deps lfx-dist/*.whl
- name: Verify Nightly Name and Version
id: verify
run: |
name=$(uv tree | grep 'langflow-base' | awk '{print $2}' | head -n 1)
version=$(uv tree | grep 'langflow-base' | awk '{print $3}' | head -n 1)
# Strip extras from package name (e.g., "langflow-base-nightly[complete]" -> "langflow-base-nightly")
name_without_extras=$(echo $name | sed 's/\[.*\]//')
if [ "$name_without_extras" != "langflow-base-nightly" ]; then
echo "Name $name_without_extras does not match langflow-base-nightly. Exiting the workflow."
exit 1
fi
if [ "$version" != "${{ inputs.nightly_tag_base }}" ]; then
echo "Version $version does not match nightly tag ${{ inputs.nightly_tag_base }}. Exiting the workflow."
exit 1
fi
# Strip the leading `v` from the version
version=$(echo $version | sed 's/^v//')
echo "version=$version" >> $GITHUB_OUTPUT
- name: Build Langflow Base for distribution
run: |
rm -rf src/backend/base/dist
rm -rf dist
make build base=true args="--no-sources --wheel"
- name: Test Langflow Base 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/
# Install with [complete] extra to test all optional dependencies
WHEEL_FILE=$(ls src/backend/base/dist/*.whl)
uv pip install "${WHEEL_FILE}[complete]"
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-nightly-base
path: src/backend/base/dist
build-nightly-main:
name: Build Langflow Nightly Main
needs: [build-nightly-base]
runs-on: ubuntu-latest
outputs:
version: ${{ steps.verify.outputs.version }}
defaults:
run:
shell: bash
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
with:
ref: ${{ inputs.nightly_tag_release}}
persist-credentials: true
- 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: Download LFX artifact
if: inputs.build_lfx
uses: actions/download-artifact@v7
with:
name: dist-nightly-lfx
path: lfx-dist
- name: Download base artifact
uses: actions/download-artifact@v7
with:
name: dist-nightly-base
path: base-dist
- name: Install the project
run: uv sync
- name: Force reinstall packages from built wheels
run: |
# While workspace resolution installs from source, we want to test the exact
# wheels that will be published to PyPI to catch any packaging issues
if [ "${{ inputs.build_lfx }}" == "true" ]; then
echo "Installing LFX from built wheel..."
uv pip install --force-reinstall --no-deps lfx-dist/*.whl
fi
echo "Installing langflow-base from built wheel..."
uv pip install --force-reinstall --no-deps base-dist/*.whl
- name: Verify Nightly Name and Version
id: verify
run: |
name=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | awk '{print $1}')
version=$(uv tree | grep 'langflow' | grep -v 'langflow-base' | awk '{print $2}')
if [ "$name" != "langflow-nightly" ]; then
echo "Name $name does not match langflow-nightly. Exiting the workflow."
exit 1
fi
if [ "$version" != "${{ inputs.nightly_tag_release }}" ]; then
echo "Version $version does not match nightly tag ${{ inputs.nightly_tag_release }}. Exiting the workflow."
exit 1
fi
# Strip the leading `v` from the version
version=$(echo $version | sed 's/^v//')
echo "version=$version" >> $GITHUB_OUTPUT
- name: Wait for PyPI Propagation
if: needs.build-nightly-base.outputs.skipped == 'false'
run: sleep 300 # wait for 5 minutes to ensure PyPI propagation of base
- name: Build Langflow Main for distribution
run: make build main=true args="--no-sources --wheel"
- name: Test Langflow Main 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-nightly-main
path: dist
test-cross-platform:
name: Test Cross-Platform Installation
needs: [build-nightly-lfx, build-nightly-base, build-nightly-main]
uses: ./.github/workflows/cross-platform-test.yml
with:
base-artifact-name: "dist-nightly-base"
main-artifact-name: "dist-nightly-main"
lfx-artifact-name: "dist-nightly-lfx"
publish-nightly-lfx:
name: Publish LFX Nightly to PyPI
needs: [build-nightly-lfx, test-cross-platform]
if: ${{ inputs.build_lfx }}
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v6
with:
ref: ${{ inputs.nightly_tag_release }}
persist-credentials: true
- name: Download LFX artifact
uses: actions/download-artifact@v7
with:
name: dist-nightly-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
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
make lfx_publish
publish-nightly-base:
name: Publish Langflow Base Nightly to PyPI
needs: [build-nightly-base, test-cross-platform, publish-nightly-lfx]
if: ${{ always() && needs.build-nightly-base.result == 'success' && needs.test-cross-platform.result == 'success' && (needs.publish-nightly-lfx.result == 'success' || inputs.build_lfx == false) }}
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.nightly_tag_release }}
persist-credentials: true
- name: Download base artifact
uses: actions/download-artifact@v7
with:
name: dist-nightly-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: needs.build-nightly-base.outputs.skipped == 'false'
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
make publish base=true
publish-nightly-main:
name: Publish Langflow Main Nightly to PyPI
needs: [build-nightly-main, test-cross-platform, publish-nightly-base]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
with:
ref: ${{ inputs.nightly_tag_release }}
persist-credentials: true
- name: Download main artifact
uses: actions/download-artifact@v7
with:
name: dist-nightly-main
path: dist
- name: Setup Environment
uses: astral-sh/setup-uv@v6
with:
enable-cache: false
python-version: "3.13"
- name: Publish to PyPI
env:
POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_API_TOKEN }}
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
run: |
make publish main=true
call_docker_build_base:
name: Call Docker Build Workflow for Langflow Base
if: ${{ always() && inputs.build_docker_base }}
needs: [build-nightly-base, build-nightly-main]
uses: ./.github/workflows/docker-nightly-build.yml
with:
ref: ${{ inputs.nightly_tag_release }}
release_type: nightly-base
push_to_registry: ${{ inputs.push_to_registry }}
secrets: inherit
call_docker_build_main:
name: Call Docker Build Workflow for Langflow
if: ${{ always() && inputs.build_docker_main }}
needs: [build-nightly-main, call_docker_build_base]
uses: ./.github/workflows/docker-nightly-build.yml
with:
ref: ${{ inputs.nightly_tag_release }}
release_type: nightly-main
push_to_registry: ${{ inputs.push_to_registry }}
secrets: inherit
# TODO: Uncomment this when our runner can fit the builds that contain pytorch (and other large dependencies)
# call_docker_build_main_all:
# name: Call Docker Build Workflow for langflow-all
# if: always() && ${{ inputs.build_docker_main == 'true' }}
# needs: [build-nightly-main]
# uses: ./.github/workflows/docker-nightly-build.yml
# with:
# ref: ${{ inputs.nightly_tag_release }}
# release_type: nightly-main-all
# main_version: ${{ inputs.nightly_tag_release }}
# secrets: inherit
# call_docker_build_main_ep:
# name: Call Docker Build Workflow for Langflow with Entrypoint
# if: ${{ always() && inputs.build_docker_ep }}
# needs: [build-nightly-main, call_docker_build_main]
# uses: ./.github/workflows/docker-build-v2.yml
# with:
# ref: ${{ inputs.nightly_tag_release }}
# release_type: main-ep
# push_to_registry: ${{ inputs.push_to_registry }}
# secrets: inherit
request-docs-review .github/workflows/request-docs-review.yml
View raw YAML
name: Request Docs Review
on:
pull_request:
types: [labeled]
jobs:
request-docs-review:
if: github.event.label.name == 'needs-docs'
runs-on: ubuntu-latest
permissions:
pull-requests: write
steps:
- uses: actions/github-script@v8
with:
script: |
const prAuthor = context.payload.pull_request.user.login;
console.log(`PR author: ${prAuthor}`);
const reviewers = '${{ vars.DOCS_TEAM_MEMBERS }}'
.split(',')
.map(u => u.trim())
.filter(u => u.length > 0 && u !== prAuthor);
if (reviewers.length > 0) {
console.log(`Requesting review from: ${reviewers.join(', ')}`);
await github.rest.pulls.requestReviewers({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: context.issue.number,
reviewers: reviewers
});
console.log('Review request sent successfully');
} else {
console.log('No eligible reviewers found (all team members may be the PR author)');
}
smoke-tests .github/workflows/smoke-tests.yml
View raw YAML
name: Smoke Tests
on:
pull_request:
types: [opened, labeled, synchronize, reopened]
release:
types: [published]
workflow_dispatch:
inputs:
ref:
description: 'Git ref to checkout (branch, tag, or commit SHA)'
required: false
default: ''
type: string
jobs:
backend-smoke-tests:
if: contains(github.event.pull_request.labels.*.name, 'smoke-test') || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
name: "Backend Smoke Tests"
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout PR
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.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 backend dependencies
run: |
uv sync --dev
- name: Run backend smoke tests (critical tests only)
run: |
uv run pytest \
src/backend/tests/unit/test_database.py \
src/backend/tests/unit/test_login.py \
src/backend/tests/unit/api/v1/test_validate.py \
src/backend/tests/unit/test_endpoints.py \
src/backend/tests/unit/api/v1/test_flows.py \
src/backend/tests/unit/test_chat_endpoint.py \
src/backend/tests/unit/api/v1/test_api_key.py \
src/backend/tests/unit/api/v1/test_endpoints.py \
src/backend/tests/unit/components/languagemodels/test_openai_model.py \
src/backend/tests/unit/components/agents/test_agent_component.py \
src/backend/tests/unit/services/tracing/test_tracing_service.py \
-m 'not api_key_required' \
--tb=short \
-v
env:
LANGFLOW_SUPERUSER: admin
LANGFLOW_SUPERUSER_PASSWORD: 123456
lfx-smoke-tests:
# created a separate label for this since it's a different test suite for LFX components
if: contains(github.event.pull_request.labels.*.name, 'smoke-test') || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
name: "LFX Smoke Tests"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout PR
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.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 lfx dependencies (isolated)
run: |
cd src/lfx
uv sync --dev
- name: Run lfx smoke tests (critical tests only)
run: |
cd src/lfx
uv run pytest \
tests/unit/graph/test_graph.py \
tests/unit/custom/component/test_component_instance_attributes.py \
tests/unit/schema/test_schema_message.py \
-m 'not api_key_required' \
--tb=short \
--maxfail=5 \
-v
env:
LANGFLOW_SUPERUSER: admin
LANGFLOW_SUPERUSER_PASSWORD: 123456
frontend-smoke-tests:
if: contains(github.event.pull_request.labels.*.name, 'smoke-test') || github.event_name == 'release' || github.event_name == 'workflow_dispatch'
name: "Frontend Smoke Tests"
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout PR
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "22"
cache: "npm"
cache-dependency-path: src/frontend/package-lock.json
- name: Install frontend dependencies
run: |
cd src/frontend
npm ci
- name: Run frontend smoke tests (unit tests only)
run: |
cd src/frontend
CI=true npx jest --ci --watchAll=false --passWithNoTests
env:
NODE_ENV: test
comment-results:
if: always() && (contains(github.event.pull_request.labels.*.name, 'smoke-test') || github.event_name == 'release' || github.event_name == 'workflow_dispatch')
name: "Comment Results"
needs: [backend-smoke-tests, lfx-smoke-tests, frontend-smoke-tests]
runs-on: ubuntu-latest
steps:
- name: Comment on PR with results
uses: actions/github-script@v8
with:
script: |
const backendStatus = '${{ needs.backend-smoke-tests.result }}';
const lfxStatus = '${{ needs.lfx-smoke-tests.result }}';
const frontendStatus = '${{ needs.frontend-smoke-tests.result }}';
const overallSuccess = backendStatus === 'success' && lfxStatus === 'success' && frontendStatus === 'success';
const emoji = overallSuccess ? '✅' : '❌';
const status = overallSuccess ? 'passed' : 'failed';
const backendEmoji = backendStatus === 'success' ? '✅' : '❌';
const lfxEmoji = lfxStatus === 'success' ? '✅' : '❌';
const frontendEmoji = frontendStatus === 'success' ? '✅' : '❌';
const comment = `${emoji} **Smoke tests ${status}**
Critical functionality validated:
- ${backendEmoji} **Backend**: 11 essential test files (database, auth, API endpoints, flows, components)
- ${lfxEmoji} **LFX**: Graph tests (langflow execution) - runs in isolation
- ${frontendEmoji} **Frontend**: Unit tests only (components, utilities)
**Coverage**: Core functionality without external dependencies
View details in the [Actions tab](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}).`;
// Only comment on PRs, not on releases or manual runs
if (context.payload.pull_request) {
await github.rest.issues.createComment({
issue_number: context.payload.pull_request.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
} else {
console.log('Smoke test results:', comment);
}
store_pytest_durations .github/workflows/store_pytest_durations.yml
View raw YAML
name: Store pytest durations
on:
workflow_dispatch:
schedule:
# Run job at 6:30 UTC every Monday (10.30pm PST/11.30pm PDT Sunday night)
- cron: "30 6 * * 1"
env:
PYTEST_RUN_PATH: "src/backend/tests"
jobs:
build:
name: Run pytest and store durations
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
ASTRA_DB_API_ENDPOINT: ${{ secrets.ASTRA_DB_API_ENDPOINT }}
ASTRA_DB_APPLICATION_TOKEN: ${{ secrets.ASTRA_DB_APPLICATION_TOKEN }}
steps:
- uses: actions/checkout@v6
- 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: Run unit tests
id: run_tests
continue-on-error: true
run: uv run pytest src/backend/tests/unit --timeout=150 --durations-path src/backend/tests/.test_durations --splitting-algorithm least_duration --store-durations
- name: Close existing PRs
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open'
});
for (const pull of pulls) {
if (pull.title === "chore: update test durations") {
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull.number,
state: 'closed'
});
}
}
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
branch-token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "chore: update test durations"
title: "chore: update test durations"
body: |
Automated PR to update test durations file.
This PR was automatically created by the store_pytest_durations workflow.
branch: update-test-durations
branch-suffix: timestamp
delete-branch: true
maintainer-can-modify: true
style-check-py matrix .github/workflows/style-check-py.yml
View raw YAML
name: Ruff Style Check
on:
pull_request:
types: [opened, synchronize, reopened, auto_merge_enabled]
paths:
- "**/*.py"
jobs:
lint:
name: Ruff Style Check
runs-on: ubuntu-latest
strategy:
matrix:
python-version:
- "3.13"
steps:
- name: Check out the code at a specific ref
uses: actions/checkout@v6
- name: "Setup Environment"
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
prune-cache: false
- name: Register problem matcher
run: echo "::add-matcher::.github/workflows/matchers/ruff.json"
- name: Run Ruff Check
run: uv run --only-dev ruff check --output-format=github .
template-tests perms .github/workflows/template-tests.yml
View raw YAML
name: Template Tests
on:
pull_request:
paths:
- 'src/backend/base/langflow/initial_setup/starter_projects/**'
- 'src/backend/tests/unit/template/test_starter_projects.py'
- 'src/backend/base/langflow/utils/template_validation.py'
- '.github/workflows/template-tests.yml'
workflow_dispatch:
permissions:
contents: read
pull-requests: read
jobs:
test-starter-projects:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
- 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 autotypescript_test matrix .github/workflows/typescript_test.yml
View raw YAML
name: Run Frontend Tests
on:
workflow_call:
secrets:
OPENAI_API_KEY:
required: true
STORE_API_KEY:
required: true
ANTHROPIC_API_KEY:
required: true
TAVILY_API_KEY:
required: true
inputs:
suites:
description: "Test suites to run (JSON array)"
required: false
type: string
default: "[]"
release:
description: "Whether this is a release build"
required: false
type: boolean
default: false
tests_folder:
description: "(Optional) Tests to run"
required: false
type: string
default: "tests"
ref:
description: "(Optional) ref to checkout"
required: false
type: string
runs-on:
description: "Runner to use for the tests"
required: false
type: string
default: "ubuntu-latest"
workflow_dispatch:
inputs:
suites:
description: "Test suites to run (JSON array)"
required: false
type: string
default: "[]"
release:
description: "Whether this is a release build"
required: false
type: boolean
default: false
tests_folder:
description: "(Optional) Tests to run"
required: false
type: string
default: "tests"
runs-on:
description: "Runner to use for the tests"
required: false
type: choice
options:
- ubuntu-latest
- windows-latest
- self-hosted
- '["self-hosted", "linux", "ARM64", "langflow-ai-arm64-40gb-ephemeral"]'
default: ubuntu-latest
env:
NODE_VERSION: "22"
PYTHON_VERSION: "3.13"
# Define the directory where Playwright browsers will be installed.
# This path is used for caching across workflows
PLAYWRIGHT_BROWSERS_PATH: "ms-playwright"
PLAYWRIGHT_VERSION: "1.57.0"
jobs:
determine-test-suite:
name: Determine Test Suites and Shard Distribution
runs-on: ${{ (inputs['runs-on'] && startsWith(format('{0}', inputs['runs-on']), '[') && fromJSON(inputs['runs-on'])) || inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
outputs:
matrix: ${{ steps.setup-matrix.outputs.matrix }}
test_grep: ${{ steps.set-matrix.outputs.test_grep }}
suites: ${{ steps.set-matrix.outputs.suites }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
fetch-depth: 0
- name: Paths Filter
id: filter
uses: dorny/paths-filter@v3
with:
filters: .github/changes-filter.yaml
- name: Determine Test Suites from Changes
id: set-matrix
shell: bash
run: |
# Start with input suites if provided, otherwise empty array
echo "Changes filter output: $(echo '${{ toJSON(steps.filter.outputs) }}')"
SUITES='${{ inputs.suites }}'
echo "Initial suites: $SUITES"
TEST_GREP=""
echo "Inputs Release: ${{ inputs.release }}"
RELEASE="${{ inputs.release || 'false' }}"
echo "Release build: $RELEASE"
# Only set to release if it's explicitly a release build
if [[ "$RELEASE" == "true" ]]; then
SUITES='["release"]'
echo "Release build detected - setting suites to: $SUITES"
# grep pattern for release is the @release tag - run all tests
TEST_GREP='--grep="@release"'
else
# If input suites were not provided, determine based on changes
if [[ "$SUITES" == "[]" ]]; then
echo "No input suites provided - determining from changes"
SUITES='[]' # Ensure we start with a valid JSON array
TAGS=()
# Add suites and tags based on changed files
if [[ "${{ steps.filter.outputs.components }}" == "true" ]]; then
SUITES=$(echo $SUITES | jq -c '. += ["components"]')
TAGS+=("@components")
echo "Added components suite"
fi
if [[ "${{ steps.filter.outputs.starter-projects }}" == "true" ]]; then
SUITES=$(echo $SUITES | jq -c '. += ["starter-projects"]')
TAGS+=("@starter-projects")
echo "Added starter-projects suite"
fi
if [[ "${{ steps.filter.outputs.workspace }}" == "true" ]]; then
SUITES=$(echo $SUITES | jq -c '. += ["workspace"]')
TAGS+=("@workspace")
echo "Added workspace suite"
fi
if [[ "${{ steps.filter.outputs.api }}" == "true" ]]; then
SUITES=$(echo $SUITES | jq -c '. += ["api"]')
TAGS+=("@api")
echo "Added api suite"
fi
if [[ "${{ steps.filter.outputs.database }}" == "true" ]]; then
SUITES=$(echo $SUITES | jq -c '. += ["database"]')
TAGS+=("@database")
echo "Added database suite"
fi
if [[ "${{ steps.filter.outputs.mainpage }}" == "true" ]]; then
SUITES=$(echo $SUITES | jq -c '. += ["mainpage"]')
TAGS+=("@mainpage")
echo "Added mainpage suite"
fi
if [[ "${{ steps.filter.outputs.development }}" == "true" ]]; then
SUITES=$(echo $SUITES | jq -c '. += ["development"]')
TAGS+=("@development")
echo "Added development suite"
fi
# Create grep pattern if we have tags
if [ ${#TAGS[@]} -gt 0 ]; then
# Join tags with | for OR logic
REGEX_PATTERN=$(IFS='|'; echo "${TAGS[*]}")
TEST_GREP='--grep="${REGEX_PATTERN}"'
else
# No path-specific changes detected - default to running release tests
# so that FE tests always run even for backend-only changes
SUITES='["release"]'
TEST_GREP='--grep="@release"'
echo "No path-specific changes detected - defaulting to release suite"
fi
else
# Process input suites to tags
# First ensure SUITES is valid JSON
if ! echo "$SUITES" | jq -e . > /dev/null 2>&1; then
echo "Warning: Input suites is not valid JSON, attempting to fix"
# Try to fix common issues like missing quotes
if [[ "$SUITES" == "[development]" ]]; then
SUITES='["development"]'
elif [[ "$SUITES" =~ ^\[(.*)\]$ ]]; then
# Extract items and add quotes
ITEMS="${BASH_REMATCH[1]}"
QUOTED_ITEMS=$(echo "$ITEMS" | sed 's/\([^,]*\)/"\1"/g')
SUITES="[$QUOTED_ITEMS]"
fi
echo "Fixed suites: $SUITES"
fi
TAGS=()
if echo "$SUITES" | jq -e 'contains(["components"])' > /dev/null; then
TAGS+=("@components")
fi
if echo "$SUITES" | jq -e 'contains(["starter-projects"])' > /dev/null; then
TAGS+=("@starter-projects")
fi
if echo "$SUITES" | jq -e 'contains(["workspace"])' > /dev/null; then
TAGS+=("@workspace")
fi
if echo "$SUITES" | jq -e 'contains(["api"])' > /dev/null; then
TAGS+=("@api")
fi
if echo "$SUITES" | jq -e 'contains(["database"])' > /dev/null; then
TAGS+=("@database")
fi
if echo "$SUITES" | jq -e 'contains(["development"])' > /dev/null; then
TAGS+=("@development")
fi
if [ ${#TAGS[@]} -gt 0 ]; then
# Join tags with | for OR logic
REGEX_PATTERN=$(IFS='|'; echo "${TAGS[*]}")
TEST_GREP='--grep "${REGEX_PATTERN}"'
fi
fi
fi
# Ensure compact JSON output
SUITES=$(echo "$SUITES" | jq -c '.')
echo "Final test suites to run: $SUITES"
echo "Test grep pattern: $TEST_GREP"
# Ensure proper JSON formatting for matrix output
echo "matrix=$(echo $SUITES | jq -c .)" >> $GITHUB_OUTPUT
echo "test_grep=$TEST_GREP" >> $GITHUB_OUTPUT
echo "suites=$SUITES" >> $GITHUB_OUTPUT
- name: Setup Node ${{ env.NODE_VERSION }}
uses: actions/setup-node@v6
id: setup-node
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
cache-dependency-path: ./src/frontend/package-lock.json
- name: Install Frontend Dependencies
run: npm ci
working-directory: ./src/frontend
- name: Calculate Test Shards Distribution
id: setup-matrix
shell: bash
run: |
cd src/frontend
# Get the test count using playwright's built-in grep
if [ -n "${{ steps.set-matrix.outputs.test_grep }}" ]; then
TEST_COUNT=$(npx playwright test ${{ inputs.tests_folder }} ${{ steps.set-matrix.outputs.test_grep }} --list | wc -l)
else
TEST_COUNT=$(npx playwright test ${{ inputs.tests_folder }} --list | wc -l)
fi
echo "Total tests to run: $TEST_COUNT"
# Calculate optimal shard count - 1 shard per 5 tests, min 1, max 70
SHARD_COUNT=$(( (TEST_COUNT + 4) / 5 ))
if [ $SHARD_COUNT -lt 1 ]; then
SHARD_COUNT=1
elif [ $SHARD_COUNT -gt 70 ]; then
SHARD_COUNT=70
fi
# Create the matrix combinations string
MATRIX_COMBINATIONS=""
for i in $(seq 1 $SHARD_COUNT); do
if [ $i -gt 1 ]; then
MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS,"
fi
MATRIX_COMBINATIONS="$MATRIX_COMBINATIONS{\"shardIndex\": $i, \"shardTotal\": $SHARD_COUNT}"
done
echo "matrix={\"include\":[$MATRIX_COMBINATIONS]}" >> "$GITHUB_OUTPUT"
setup-and-test:
name: Playwright Tests - Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
runs-on: ${{ (inputs['runs-on'] && startsWith(format('{0}', inputs['runs-on']), '[') && fromJSON(inputs['runs-on'])) || inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
if: ${{ needs.determine-test-suite.outputs.test_grep != '' }}
needs: determine-test-suite
strategy:
fail-fast: false
matrix: ${{ fromJson(needs.determine-test-suite.outputs.matrix) }}
env:
OPENAI_API_KEY: ${{ inputs.openai_api_key || secrets.OPENAI_API_KEY }}
STORE_API_KEY: ${{ inputs.store_api_key || secrets.STORE_API_KEY }}
SEARCH_API_KEY: "${{ secrets.SEARCH_API_KEY }}"
ASTRA_DB_APPLICATION_TOKEN: "${{ secrets.ASTRA_DB_APPLICATION_TOKEN }}"
ASTRA_DB_API_ENDPOINT: "${{ secrets.ASTRA_DB_API_ENDPOINT }}"
ANTHROPIC_API_KEY: "${{ secrets.ANTHROPIC_API_KEY }}"
TAVILY_API_KEY: "${{ secrets.TAVILY_API_KEY }}"
LANGFLOW_DEACTIVE_TRACING: "true"
outputs:
failed: ${{ steps.check-failure.outputs.failed }}
steps:
- name: Checkout Repository
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node.js Environment
uses: actions/setup-node@v6
id: setup-node
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
cache-dependency-path: ./src/frontend/package-lock.json
- name: Install Frontend Dependencies
run: npm ci
working-directory: ./src/frontend
# Cache Playwright browsers using a composite key
- name: Cache Playwright Browsers
id: cache-playwright
uses: actions/cache@v5
with:
path: ${{ env.PLAYWRIGHT_BROWSERS_PATH }}
key: playwright-${{ env.PLAYWRIGHT_VERSION }}-chromium-${{ runner.os }}
restore-keys: |
playwright-${{ env.PLAYWRIGHT_VERSION }}-chromium-${{ runner.os }}
- name: Install Playwright Browser Dependencies (Linux)
if: steps.cache-playwright.outputs.cache-hit != 'true' && runner.os == 'Linux' && !contains(inputs.runs-on, 'self-hosted')
shell: bash
run: |
cd ./src/frontend
npx playwright install --with-deps chromium
- name: Install Playwright Browsers (Windows/macOS/Self-Hosted)
if: steps.cache-playwright.outputs.cache-hit != 'true' && (runner.os != 'Linux' || contains(inputs.runs-on, 'self-hosted'))
shell: bash
run: |
cd ./src/frontend
npx playwright install chromium
- 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 Python Dependencies
run: uv sync --extra audio
- name: Configure Environment Variables
shell: bash
run: |
touch .env
echo "${{ secrets.ENV_VARS }}" > .env
- name: Execute Playwright Tests
uses: nick-fields/retry@v3
with:
timeout_minutes: 12
max_attempts: 2
command: |
cd src/frontend
echo 'Running tests with pattern: ${{ needs.determine-test-suite.outputs.test_grep }}'
npx playwright test ${{ inputs.tests_folder }} ${{ needs.determine-test-suite.outputs.test_grep }} --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --list --retries=3
# echo command before running
echo "npx playwright test ${{ inputs.tests_folder }} ${{ needs.determine-test-suite.outputs.test_grep }} --trace on --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers 2 --retries=3"
npx playwright test ${{ inputs.tests_folder }} ${{ needs.determine-test-suite.outputs.test_grep }} --trace on --shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --workers 2 --retries=3
- name: Upload Test Results
if: always()
uses: actions/upload-artifact@v6
with:
name: blob-report-${{ runner.os }}-${{ matrix.shardIndex }}
path: src/frontend/blob-report
retention-days: 1
- name: Cleanup UV Cache
run: uv cache prune --ci
merge-reports:
# We need to repeat the condition at every step
# https://github.com/actions/runner/issues/662
needs: setup-and-test
runs-on: ${{ (inputs['runs-on'] && startsWith(format('{0}', inputs['runs-on']), '[') && fromJSON(inputs['runs-on'])) || inputs['runs-on'] || github.event.inputs['runs-on'] || 'ubuntu-latest' }}
if: always()
env:
EXIT_CODE: ${{!contains(needs.setup-and-test.result, 'failure') && !contains(needs.setup-and-test.result, 'cancelled') && '0' || '1'}}
steps:
- name: "Should Merge Reports"
# If the CI was successful, we don't need to merge the reports
# so we can skip all the steps below
id: should_merge_reports
shell: bash
run: |
if [ "$EXIT_CODE" == "0" ]; then
echo "should_merge_reports=false" >> $GITHUB_OUTPUT
else
echo "should_merge_reports=true" >> $GITHUB_OUTPUT
fi
- name: Checkout code
if: ${{ steps.should_merge_reports.outputs.should_merge_reports == 'true' }}
uses: actions/checkout@v6
with:
ref: ${{ inputs.ref || github.ref }}
- name: Setup Node.js
if: ${{ steps.should_merge_reports.outputs.should_merge_reports == 'true' }}
uses: actions/setup-node@v6
with:
node-version: ${{ env.NODE_VERSION }}
- name: Download blob reports from GitHub Actions Artifacts
if: ${{ steps.should_merge_reports.outputs.should_merge_reports == 'true' }}
uses: actions/download-artifact@v7
with:
path: all-blob-reports
pattern: blob-report-*
merge-multiple: true
- name: Merge into HTML Report
if: ${{ steps.should_merge_reports.outputs.should_merge_reports == 'true' }}
shell: bash
run: |
npx playwright merge-reports --reporter html ./all-blob-reports
- name: Upload HTML report
if: ${{ steps.should_merge_reports.outputs.should_merge_reports == 'true' }}
uses: actions/upload-artifact@v6
with:
name: html-report--attempt-${{ github.run_attempt }}
path: playwright-report
retention-days: 14