diff --git a/.github/workflows/lint-backend.yml b/.github/workflows/lint-backend.yml index f8955faed..702afff87 100644 --- a/.github/workflows/lint-backend.yml +++ b/.github/workflows/lint-backend.yml @@ -2,12 +2,7 @@ name: Lint Backend on: pull_request: - paths: - - 'src/**/*.py' - - 'tests/**/*.py' - - 'pyproject.toml' - - 'uv.lock' - - '.github/workflows/lint-backend.yml' + merge_group: # Cancel in-flight runs on the same PR when a new push lands. concurrency: @@ -15,7 +10,40 @@ concurrency: cancel-in-progress: true jobs: + changes: + runs-on: ubuntu-latest + outputs: + run: ${{ steps.decision.outputs.run }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + if: github.event_name == 'pull_request' + with: + filters: | + relevant: + - 'src/**/*.py' + - 'tests/**/*.py' + - 'pyproject.toml' + - 'uv.lock' + - '.github/workflows/lint-backend.yml' + - id: decision + run: | + # Skip on merge_group: lint runs on changed files only, so + # re-running on the queued ref would just re-check the same set + # the PR already passed. The required job below still reports + # green so the merge queue is not blocked. + if [ "${{ github.event_name }}" = "merge_group" ]; then + echo "run=false" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.filter.outputs.relevant }}" = "true" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + lint: + needs: changes + if: needs.changes.outputs.run == 'true' name: Ruff and mypy on changed files runs-on: ubuntu-latest @@ -87,3 +115,17 @@ jobs: if [ "${#test_files[@]}" -gt 0 ]; then uv run mypy "${test_files[@]}" fi + + required: + needs: lint + if: always() + runs-on: ubuntu-latest + steps: + - name: Verify lint result + run: | + result="${{ needs.lint.result }}" + if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + echo "lint job did not succeed: $result" + exit 1 + fi + echo "lint result: $result (success or skipped — passing)" diff --git a/.github/workflows/lint-frontend.yml b/.github/workflows/lint-frontend.yml index 0e7a9d70c..5d0f8a7d9 100644 --- a/.github/workflows/lint-frontend.yml +++ b/.github/workflows/lint-frontend.yml @@ -2,12 +2,36 @@ name: Frontend Lint on: pull_request: - paths: - - 'frontend/**' - - '.github/workflows/lint-frontend.yml' + merge_group: jobs: + changes: + runs-on: ubuntu-latest + outputs: + run: ${{ steps.decision.outputs.run }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + if: github.event_name == 'pull_request' + with: + filters: | + relevant: + - 'frontend/**' + - '.github/workflows/lint-frontend.yml' + - id: decision + run: | + if [ "${{ github.event_name }}" = "merge_group" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.filter.outputs.relevant }}" = "true" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + biome: + needs: changes + if: needs.changes.outputs.run == 'true' name: Biome runs-on: ubuntu-latest steps: @@ -48,3 +72,16 @@ jobs: - name: TypeScript type check working-directory: frontend run: npm run typecheck + required: + needs: biome + if: always() + runs-on: ubuntu-latest + steps: + - name: Verify biome result + run: | + result="${{ needs.biome.result }}" + if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + echo "biome job did not succeed: $result" + exit 1 + fi + echo "biome result: $result (success or skipped — passing)" diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml index 630024dd1..bb9a41d1a 100644 --- a/.github/workflows/test-e2e.yml +++ b/.github/workflows/test-e2e.yml @@ -2,18 +2,16 @@ name: E2E Tests on: pull_request: - paths: - - 'src/**' - - 'frontend/**' - - 'tests/**' - - 'scripts/**' - - 'flows/**' - - 'docker-compose.yml' - - 'Dockerfile*' - - 'Makefile' - - '.github/workflows/test-e2e.yml' + merge_group: workflow_dispatch: +# Serialize E2E and Integration on the same PR/queue ref. They share runner +# pool and Docker resources; running them concurrently inflates the flake +# rate. Different PRs have different refs and run in parallel as usual. +concurrency: + group: heavy-tests-${{ github.ref }} + cancel-in-progress: false + env: NODE_VERSION: "22" PYTHON_VERSION: "3.13" @@ -23,7 +21,40 @@ env: PLAYWRIGHT_VERSION: "1.57.0" jobs: + changes: + runs-on: ubuntu-latest + outputs: + run: ${{ steps.decision.outputs.run }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + if: github.event_name == 'pull_request' + with: + filters: | + relevant: + - 'src/**' + - 'frontend/**' + - 'tests/**' + - 'scripts/**' + - 'flows/**' + - 'docker-compose.yml' + - 'Dockerfile*' + - 'Makefile' + - '.github/workflows/test-e2e.yml' + - id: decision + run: | + if [ "${{ github.event_name }}" = "merge_group" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.filter.outputs.relevant }}" = "true" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + e2e: + needs: changes + if: needs.changes.outputs.run == 'true' runs-on: labels: ["self-hosted", "linux", "ARM64", "langflow-ai-arm64-40gb-ephemeral-sudo"] env: @@ -99,7 +130,7 @@ jobs: - name: Run Playwright tests - working-directory: frontend + uses: nick-fields/retry@v3 env: CI: "true" OPENSEARCH_HOST: localhost @@ -108,7 +139,18 @@ jobs: OPENSEARCH_PASSWORD: ${{ env.OPENSEARCH_PASSWORD }} GOOGLE_OAUTH_CLIENT_ID: "" GOOGLE_OAUTH_CLIENT_SECRET: "" - run: npx playwright test + with: + timeout_minutes: 60 + max_attempts: 2 + retry_on: error + command: | + cd frontend + npx playwright test + on_retry_command: | + # Services are still up — Playwright handles its own fixture + # lifecycle. Just clear stale artifacts so the retry's report + # isn't contaminated by the first attempt's output. + rm -rf frontend/test-results frontend/playwright-report || true - name: Collect service logs on failure if: failure() @@ -150,3 +192,17 @@ jobs: docker rm -f openrag-backend-proxy 2>/dev/null || true make clean || true docker system prune -f || true + + required: + needs: e2e + if: always() + runs-on: ubuntu-latest + steps: + - name: Verify e2e result + run: | + result="${{ needs.e2e.result }}" + if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + echo "e2e job did not succeed: $result" + exit 1 + fi + echo "e2e result: $result (success or skipped — passing)" diff --git a/.github/workflows/test-integration.yml b/.github/workflows/test-integration.yml index 562582e38..4bcf82410 100644 --- a/.github/workflows/test-integration.yml +++ b/.github/workflows/test-integration.yml @@ -2,14 +2,7 @@ name: Integration Tests on: pull_request: - paths: - - 'src/**.py' - - 'tests/**.py' - - 'pyproject.toml' - - 'uv.lock' - - 'sdks/**' - - 'flows/**' - - '.github/workflows/test-integration.yml' + merge_group: workflow_dispatch: inputs: use_local_images: @@ -18,8 +11,46 @@ on: type: boolean default: true +# Serialize E2E and Integration on the same PR/queue ref. They share runner +# pool and Docker resources; running them concurrently inflates the flake +# rate. Different PRs have different refs and run in parallel as usual. +concurrency: + group: heavy-tests-${{ github.ref }} + cancel-in-progress: false + jobs: + changes: + runs-on: ubuntu-latest + outputs: + run: ${{ steps.decision.outputs.run }} + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v3 + id: filter + if: github.event_name == 'pull_request' + with: + filters: | + relevant: + - 'src/**.py' + - 'tests/**.py' + - 'pyproject.toml' + - 'uv.lock' + - 'sdks/**' + - 'flows/**' + - '.github/workflows/test-integration.yml' + - id: decision + run: | + if [ "${{ github.event_name }}" = "merge_group" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + elif [ "${{ steps.filter.outputs.relevant }}" = "true" ]; then + echo "run=true" >> "$GITHUB_OUTPUT" + else + echo "run=false" >> "$GITHUB_OUTPUT" + fi + tests: + needs: changes + if: needs.changes.outputs.run == 'true' runs-on: labels: ["self-hosted", "linux", "ARM64", "langflow-ai-arm64-40gb-ephemeral-sudo"] env: @@ -54,7 +85,7 @@ jobs: - name: Checkout uses: actions/checkout@v4 - + - name: Verify workspace run: | echo "Current directory: $(pwd)" @@ -78,6 +109,7 @@ jobs: run: uv sync - name: Run integration tests + uses: nick-fields/retry@v3 env: OPENSEARCH_HOST: localhost OPENSEARCH_PORT: 9200 @@ -89,16 +121,42 @@ jobs: GOOGLE_OAUTH_CLIENT_SECRET: "" # Disable startup ingest noise unless a test enables it DISABLE_STARTUP_INGEST: "true" + with: + timeout_minutes: 45 + max_attempts: 2 + retry_on: error + command: | + # For PRs and merge_group, always build locally since we're + # testing new code. For workflow_dispatch, use the input + # (defaults to true). + USE_LOCAL="${{ inputs.use_local_images }}" + if [ "${{ github.event_name }}" != "workflow_dispatch" ] || [ "$USE_LOCAL" != "false" ]; then + echo "Running tests with locally built images..." + make test-ci-local + else + echo "Running tests with DockerHub images..." + make test-ci + fi + echo "Keys directory after tests:" + ls -la keys/ || echo "No keys directory" + on_retry_command: | + # Heavyweight reset between attempts — the make target brings + # up the full docker-compose stack, and stale containers / + # volumes from the first attempt will poison the retry. + echo "Integration test attempt failed; tearing down before retry..." + docker compose -f docker-compose.yml down -v --remove-orphans || true + docker run --rm -v $(pwd):/work alpine sh -c "rm -rf /work/opensearch-data /work/config /work/langflow-data /work/keys /work/data /work/flows /work/openrag-documents" || true + + required: + needs: tests + if: always() + runs-on: ubuntu-latest + steps: + - name: Verify tests result run: | - # For PRs, always build locally since we're testing new code - # For workflow_dispatch, use the input (defaults to true) - USE_LOCAL="${{ inputs.use_local_images }}" - if [ "${{ github.event_name }}" == "pull_request" ] || [ "$USE_LOCAL" != "false" ]; then - echo "Running tests with locally built images..." - make test-ci-local - else - echo "Running tests with DockerHub images..." - make test-ci + result="${{ needs.tests.result }}" + if [ "$result" = "failure" ] || [ "$result" = "cancelled" ]; then + echo "tests job did not succeed: $result" + exit 1 fi - echo "Keys directory after tests:" - ls -la keys/ || echo "No keys directory" + echo "tests result: $result (success or skipped — passing)"