name: DB Migration Validation on: workflow_call: inputs: nightly_tag: description: "Nightly tag to test migration to" required: true type: string workflow_dispatch: inputs: nightly_tag: description: "Nightly tag to test migration to (e.g., langflowai/langflow-nightly:latest)" required: false type: string default: "langflowai/langflow-nightly:latest" # Note: This workflow is called by nightly_build.yml after Docker images are built env: PYTHON_VERSION: "3.13" POSTGRES_DB: langflow_test POSTGRES_USER: langflow POSTGRES_PASSWORD: langflow_test_pass # pragma: allowlist secret jobs: migration-pip-venv: name: "Migration Test: pip/venv (stable → nightly)" runs-on: ubuntu-latest timeout-minutes: 30 services: postgres: image: postgres:16 env: POSTGRES_DB: ${{ env.POSTGRES_DB }} POSTGRES_USER: ${{ env.POSTGRES_USER }} POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 steps: - name: Setup Python uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - name: Install uv uses: astral-sh/setup-uv@v6 with: enable-cache: false - name: Create test directory run: | mkdir -p migration-test cd migration-test - name: Install latest stable Langflow with PostgreSQL working-directory: migration-test run: | echo "Installing latest stable langflow[postgresql]..." uv venv source .venv/bin/activate uv pip install "langflow[postgresql]" # Verify installation python -c 'from importlib.metadata import version; print("Installed Langflow version:", version("langflow"))' - name: Initialize database with stable version working-directory: migration-test env: LANGFLOW_DATABASE_URL: postgresql://${{ env.POSTGRES_USER }}:${{ env.POSTGRES_PASSWORD }}@localhost:5432/${{ env.POSTGRES_DB }} # pragma: allowlist secret run: | source .venv/bin/activate echo "Starting Langflow to initialize database..." # shellcheck disable=SC2016 timeout 120 bash -c ' python -m langflow run --host 127.0.0.1 --port 7860 --backend-only & LANGFLOW_PID=$! until curl -f http://127.0.0.1:7860/health_check 2>/dev/null; do sleep 2 done kill $LANGFLOW_PID wait $LANGFLOW_PID 2>/dev/null || true ' || { echo "Failed to start Langflow stable version" exit 1 } echo "Database initialized successfully with stable version" - name: Create witness flow working-directory: migration-test env: LANGFLOW_DATABASE_URL: postgresql://${{ env.POSTGRES_USER }}:${{ env.POSTGRES_PASSWORD }}@localhost:5432/${{ env.POSTGRES_DB }} run: | source .venv/bin/activate # Start Langflow briefly to create test data python -m langflow run --host 127.0.0.1 --port 7860 --backend-only & LANGFLOW_PID=$! # Wait for startup timeout 60 bash -c 'until curl -f http://127.0.0.1:7860/health_check 2>/dev/null; do sleep 2; done' # Get auth token via auto_login (works under default AUTO_LOGIN=true, no credentials needed) TOKEN=$(curl -fsS http://127.0.0.1:7860/api/v1/auto_login | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])") if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then echo "❌ Failed to get authentication token" kill $LANGFLOW_PID exit 1 fi echo "✅ Authentication token obtained" # Create a witness flow (proves row persistence) curl -fsSL -X POST http://127.0.0.1:7860/api/v1/flows/ \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "name": "Migration Witness Flow", "description": "Test flow to verify data persistence across migration", "data": {"nodes": [], "edges": []} }' > flow_response.json if ! grep -q '"id"' flow_response.json; then echo "❌ Flow creation failed - no id in response" cat flow_response.json exit 1 fi echo "✅ Witness flow created successfully" # Create a witness credential (type=Credential stores encrypted value, exercises encrypted-column migrations) curl -fsSL -X POST http://127.0.0.1:7860/api/v1/variables/ \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"name": "migration_witness_secret", "value": "witness_value_12345", "type": "Credential", "default_fields": []}' > variable_response.json # pragma: allowlist secret if ! grep -q '"id"' variable_response.json; then echo "❌ Credential creation failed - no id in response" cat variable_response.json exit 1 fi echo "✅ Witness credential created successfully" # Stop Langflow kill $LANGFLOW_PID wait $LANGFLOW_PID 2>/dev/null || true echo "Witness data created (flow + credential)" - name: Upgrade to nightly version working-directory: migration-test run: | source .venv/bin/activate NIGHTLY_TAG="${{ inputs.nightly_tag || 'langflowai/langflow-nightly:latest' }}" echo "Upgrading to nightly version: $NIGHTLY_TAG" # Remove the stable install first. The nightly now publishes as a `.devN` pre-release of # the canonical `langflow` (same distribution as stable, different version), so a clean # uninstall avoids stale files and guarantees the dev version is what boots — otherwise # `python -m langflow` could keep running the stable version and no real nightly migration # is exercised (false-positive test). echo "Removing stable langflow to force a clean install of the nightly dev version..." uv pip uninstall -y langflow langflow-base || true # Extract version from Docker tag (format: langflowai/langflow-nightly:v1.10.0.dev20260522) if [[ "$NIGHTLY_TAG" == *":"* ]]; then VERSION="${NIGHTLY_TAG##*:}" echo "Extracted version from tag: $VERSION" # Strip 'v' prefix if present (PyPI doesn't use 'v' prefix) VERSION="${VERSION#v}" echo "Version for PyPI: $VERSION" # Scope pre-release resolution to the langflow stack only. uv accepts a # pre-release for a package when its own requirement carries a pre-release # marker, but NOT via transitive pins (langflow -> langflow-base -> lfx -> # langflow-sdk are exact ==devN pins on nightlies), so each lockstep package # must be requested directly. A global --prerelease=allow is NOT safe here: # it lets unrelated dependencies resolve to alphas (pydantic 2.14.0a1 broke # langchain-core imports on the first canonical-prerelease nightly). # langflow-sdk has its own version line, so an explicit .dev0 floor marks it # pre-release-eligible while the lfx pin selects the exact version. if [[ "$VERSION" == "latest" ]]; then # Install latest nightly (canonical pre-release) from PyPI uv pip install --upgrade 'langflow[postgresql]>=0.0.0.dev0' 'langflow-base>=0.0.0.dev0' 'lfx>=0.0.0.dev0' 'langflow-sdk>=0.0.0.dev0' else # Install specific version uv pip install --upgrade "langflow[postgresql]==$VERSION" "langflow-base==$VERSION" "lfx==$VERSION" 'langflow-sdk>=0.0.0.dev0' fi else # Direct version string (strip 'v' prefix if present) VERSION="${NIGHTLY_TAG#v}" uv pip install --upgrade "langflow[postgresql]==$VERSION" "langflow-base==$VERSION" "lfx==$VERSION" 'langflow-sdk>=0.0.0.dev0' fi # Verify upgrade python -c 'from importlib.metadata import version; print("Upgraded to Langflow Nightly version:", version("langflow"))' - name: Run migration and verify startup working-directory: migration-test env: LANGFLOW_DATABASE_URL: postgresql://${{ env.POSTGRES_USER }}:${{ env.POSTGRES_PASSWORD }}@localhost:5432/${{ env.POSTGRES_DB }} # pragma: allowlist secret run: | source .venv/bin/activate echo "Starting Langflow nightly to run migrations..." # shellcheck disable=SC2016 timeout 180 bash -c ' python -m langflow run --host 127.0.0.1 --port 7860 --backend-only > langflow_nightly.log 2>&1 & LANGFLOW_PID=$! until curl -f http://127.0.0.1:7860/health_check 2>/dev/null; do if ! kill -0 $LANGFLOW_PID 2>/dev/null; then echo "Langflow process died during startup" cat langflow_nightly.log exit 1 fi sleep 2 done echo "Langflow nightly started successfully" kill $LANGFLOW_PID wait $LANGFLOW_PID 2>/dev/null || true ' || { echo "Failed to start Langflow nightly version" cat langflow_nightly.log || true exit 1 } - name: Verify witness data persisted working-directory: migration-test env: LANGFLOW_DATABASE_URL: postgresql://${{ env.POSTGRES_USER }}:${{ env.POSTGRES_PASSWORD }}@localhost:5432/${{ env.POSTGRES_DB }} run: | source .venv/bin/activate # Start Langflow to query data python -m langflow run --host 127.0.0.1 --port 7860 --backend-only & LANGFLOW_PID=$! timeout 60 bash -c 'until curl -f http://127.0.0.1:7860/health_check 2>/dev/null; do sleep 2; done' # Get auth token via auto_login (works under default AUTO_LOGIN=true, no credentials needed) TOKEN=$(curl -fsS http://127.0.0.1:7860/api/v1/auto_login | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])") if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then echo "❌ Failed to get authentication token" kill $LANGFLOW_PID exit 1 fi echo "✅ Authentication token obtained" # Verify witness flow persisted curl -fsSL --compressed http://127.0.0.1:7860/api/v1/flows/ \ -H "Authorization: Bearer $TOKEN" > flows_after_migration.json if grep -q "Migration Witness Flow" flows_after_migration.json; then echo "✅ Witness flow found after migration" else echo "❌ Witness flow NOT found after migration" cat flows_after_migration.json kill $LANGFLOW_PID exit 1 fi # Verify witness credential persisted (confirms encrypted-column migration ran without data loss) curl -fsSL --compressed http://127.0.0.1:7860/api/v1/variables/ \ -H "Authorization: Bearer $TOKEN" > variables_after_migration.json if grep -q "migration_witness_secret" variables_after_migration.json; then echo "✅ Witness credential found after migration" else echo "❌ Witness credential NOT found after migration" cat variables_after_migration.json kill $LANGFLOW_PID exit 1 fi kill $LANGFLOW_PID wait $LANGFLOW_PID 2>/dev/null || true - name: Upload logs on failure if: failure() uses: actions/upload-artifact@v6 with: name: migration-pip-venv-logs path: | migration-test/*.log migration-test/*.json retention-days: 7 migration-docker-compose: name: "Migration Test: Docker Compose (stable → nightly)" runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v6 - name: Create Docker Compose test directory run: | mkdir -p docker-migration-test cd docker-migration-test - name: Create Docker Compose file for stable working-directory: docker-migration-test run: | # pragma: allowlist secret cat > docker-compose.yml <<'EOF' services: langflow: image: langflowai/langflow:latest ports: - "7860:7860" environment: # pragma: allowlist secret - LANGFLOW_DATABASE_URL=postgresql://langflow:langflow@postgres:5432/langflow # pragma: allowlist secret depends_on: postgres: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:7860/health_check"] interval: 10s timeout: 5s retries: 10 postgres: image: postgres:16 environment: # pragma: allowlist secret - POSTGRES_USER=langflow - POSTGRES_PASSWORD=langflow # pragma: allowlist secret - POSTGRES_DB=langflow volumes: - langflow_postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U langflow"] interval: 10s timeout: 5s retries: 5 volumes: langflow_postgres_data: EOF - name: Start stable Langflow with Docker Compose working-directory: docker-migration-test run: | echo "Starting Langflow stable version..." docker compose up -d echo "Waiting for Langflow to be healthy..." timeout 180 bash -c 'until docker compose exec -T langflow curl -f http://localhost:7860/health_check 2>/dev/null; do sleep 5; done' || { echo "Langflow stable failed to start" docker compose logs exit 1 } echo "Langflow stable is running" - name: Create witness flow via API working-directory: docker-migration-test run: | # Get auth token via auto_login (works under default AUTO_LOGIN=true, no credentials needed) TOKEN=$(curl -fsS http://localhost:7860/api/v1/auto_login | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])") if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then echo "❌ Failed to get authentication token" exit 1 fi echo "✅ Authentication token obtained" echo "Creating witness flow..." curl -fsSL -X POST http://localhost:7860/api/v1/flows/ \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{ "name": "Docker Migration Witness Flow", "description": "Test flow for Docker Compose migration", "data": {"nodes": [], "edges": []} }' > flow_response.json if ! grep -q '"id"' flow_response.json; then echo "❌ Flow creation failed - no id in response" cat flow_response.json exit 1 fi echo "✅ Witness flow created successfully" echo "Creating witness credential (exercises encrypted-column migrations)..." curl -fsSL -X POST http://localhost:7860/api/v1/variables/ \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $TOKEN" \ -d '{"name": "migration_witness_secret", "value": "witness_value_12345", "type": "Credential", "default_fields": []}' > variable_response.json # pragma: allowlist secret if ! grep -q '"id"' variable_response.json; then echo "❌ Credential creation failed - no id in response" cat variable_response.json exit 1 fi echo "✅ Witness credential created successfully" - name: Stop stable Langflow (keep PostgreSQL volume) working-directory: docker-migration-test run: | echo "Stopping Langflow stable..." docker compose stop langflow docker compose rm -f langflow - name: Update to nightly image working-directory: docker-migration-test run: | NIGHTLY_TAG="${{ inputs.nightly_tag || 'langflowai/langflow-nightly:latest' }}" # Strip 'v' prefix from version part — Docker images are published without it # e.g. langflowai/langflow-nightly:v1.10.0.dev20260522 → langflowai/langflow-nightly:1.10.0.dev20260522 VERSION_PART="${NIGHTLY_TAG##*:}" IMAGE_NAME="${NIGHTLY_TAG%%:*}" DOCKER_TAG="${IMAGE_NAME}:${VERSION_PART#v}" echo "Updating to nightly: $DOCKER_TAG" # Update docker-compose.yml to use nightly image sed -i "s|image: langflowai/langflow:latest|image: $DOCKER_TAG|" docker-compose.yml cat docker-compose.yml - name: Start nightly Langflow with same PostgreSQL volume working-directory: docker-migration-test run: | echo "Starting Langflow nightly version..." docker compose up -d langflow echo "Waiting for Langflow nightly to be healthy..." timeout 180 bash -c 'until docker compose exec -T langflow curl -f http://localhost:7860/health_check 2>/dev/null; do sleep 5; done' || { echo "Langflow nightly failed to start" docker compose logs langflow exit 1 } echo "Langflow nightly started successfully" - name: Verify witness data persisted working-directory: docker-migration-test run: | # Get auth token via auto_login (works under default AUTO_LOGIN=true, no credentials needed) TOKEN=$(curl -fsS http://localhost:7860/api/v1/auto_login | python3 -c "import json,sys; print(json.load(sys.stdin)['access_token'])") if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then echo "❌ Failed to get authentication token" exit 1 fi echo "✅ Authentication token obtained" echo "Verifying witness flow persisted..." curl -fsSL --compressed http://localhost:7860/api/v1/flows/ \ -H "Authorization: Bearer $TOKEN" > flows_after_migration.json if grep -q "Docker Migration Witness Flow" flows_after_migration.json; then echo "✅ Witness flow found after Docker migration" else echo "❌ Witness flow NOT found after Docker migration" cat flows_after_migration.json exit 1 fi echo "Verifying witness credential persisted (confirms encrypted-column migration ran without data loss)..." curl -fsSL --compressed http://localhost:7860/api/v1/variables/ \ -H "Authorization: Bearer $TOKEN" > variables_after_migration.json if grep -q "migration_witness_secret" variables_after_migration.json; then echo "✅ Witness credential found after Docker migration" else echo "❌ Witness credential NOT found after Docker migration" cat variables_after_migration.json exit 1 fi - name: Collect logs on failure if: failure() working-directory: docker-migration-test run: | docker compose logs > docker-compose-logs.txt - name: Upload logs on failure if: failure() uses: actions/upload-artifact@v6 with: name: migration-docker-compose-logs path: | docker-migration-test/*.log docker-migration-test/*.json docker-migration-test/*.txt retention-days: 7 - name: Cleanup if: always() working-directory: docker-migration-test run: | docker compose down -v