name: Database Migration Validation on: pull_request: paths: - 'src/backend/base/langflow/alembic/**' - 'src/backend/base/langflow/services/database/models/**' - 'src/backend/base/langflow/services/database/service.py' - 'src/backend/tests/unit/alembic/**' - '.github/workflows/migration-validation.yml' jobs: model-migration-consistency: name: Model/Migration Consistency runs-on: ubuntu-latest services: postgres: image: postgres:16 env: POSTGRES_USER: langflow POSTGRES_PASSWORD: langflow POSTGRES_DB: langflow ports: - 5432:5432 options: >- --health-cmd="pg_isready -U langflow" --health-interval=10s --health-timeout=5s --health-retries=5 steps: - name: Checkout code uses: actions/checkout@v6 with: fetch-depth: 0 - name: Install uv uses: astral-sh/setup-uv@v6 - name: Setup Python uses: actions/setup-python@v6 with: python-version: '3.12' - name: Install dependencies run: | uv sync --extra postgresql - name: Check model/migration consistency env: MIGRATION_VALIDATION_CI: "true" LANGFLOW_TEST_DATABASE_URI: "postgresql://langflow:langflow@localhost:5432/langflow" run: | uv run pytest src/backend/tests/unit/alembic/test_migration_execution.py -x -v validate-migration: name: Migration Pattern Validation 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.12' - name: Get changed migration files id: changed-files run: | set -euo pipefail CHANGED_FILES=$(git diff --name-only origin/main...HEAD | grep -E 'src/backend/base/langflow/alembic/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" echo "files=$(printf '%s' "$CHANGED_FILES" | tr '\n' ' ')" >> "$GITHUB_OUTPUT" fi - name: Validate migration patterns if: steps.changed-files.outputs.files != '' env: MIGRATION_FILES: ${{ steps.changed-files.outputs.files }} run: | python src/backend/base/langflow/alembic/migration_validator.py $MIGRATION_FILES - name: Generate validation report if: always() && steps.changed-files.outputs.files != '' env: MIGRATION_FILES: ${{ steps.changed-files.outputs.files }} run: | python src/backend/base/langflow/alembic/migration_validator.py \ --json $MIGRATION_FILES > validation-report.json 2> validation-stderr.txt || true if [ ! -s validation-report.json ]; then echo "::error::Validator produced no output. Stderr:" cat validation-stderr.txt fi - 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`; validationPassed = false; } // Post or update comment (non-critical — don't let API errors mask validation results) try { 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 }); } } catch (apiError) { core.warning(`Failed to post PR comment: ${apiError.message}`); } // Fail the workflow if validation didn't pass if (!validationPassed) { core.setFailed('Migration validation failed'); }