From fa184c3103e46eaef7fcda405ca60e4f76ae3613 Mon Sep 17 00:00:00 2001 From: Vishal Bala Date: Fri, 10 Apr 2026 10:23:33 +0200 Subject: [PATCH 1/4] fix: handle Dependabot external PR test workflows --- .github/workflows/test-fork-pr.yml | 38 +++++++++++++++++++++--------- .github/workflows/test.yml | 2 +- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test-fork-pr.yml b/.github/workflows/test-fork-pr.yml index 779e0118b..2283a67c4 100644 --- a/.github/workflows/test-fork-pr.yml +++ b/.github/workflows/test-fork-pr.yml @@ -1,4 +1,4 @@ -name: Test Fork PR +name: Test External PR on: workflow_dispatch: @@ -22,6 +22,7 @@ jobs: name: Validate PR runs-on: ubuntu-latest outputs: + pr_number: ${{ steps.pr-info.outputs.pr_number }} head_sha: ${{ steps.pr-info.outputs.head_sha }} head_repo: ${{ steps.pr-info.outputs.head_repo }} steps: @@ -41,11 +42,17 @@ jobs: return; } - if (pr.data.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}`) { - core.setFailed(`PR #${{ inputs.pr_number }} is not from a fork. Use the regular workflow.`); + const isForkPr = pr.data.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`; + const isDependabotPr = pr.data.user.login === 'dependabot[bot]'; + + if (!isForkPr && !isDependabotPr) { + core.setFailed( + `PR #${{ inputs.pr_number }} is neither from a fork nor authored by dependabot[bot]. Use the regular workflow.` + ); return; } + core.setOutput('pr_number', String(pr.data.number)); core.setOutput('head_sha', pr.data.head.sha); core.setOutput('head_repo', pr.data.head.repo.full_name); @@ -63,21 +70,29 @@ jobs: uses: actions/github-script@v7 with: script: | + const runUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; const check = await github.rest.checks.create({ owner: context.repo.owner, repo: context.repo.repo, - name: 'Fork PR - Service Tests', + name: 'External PR - Service Tests', head_sha: '${{ needs.setup.outputs.head_sha }}', status: 'in_progress', - started_at: new Date().toISOString() + started_at: new Date().toISOString(), + details_url: runUrl, + output: { + title: 'External PR Service Tests Running', + summary: `Service tests are in progress for PR #${{ needs.setup.outputs.pr_number }} at commit ${{ needs.setup.outputs.head_sha }}.`, + text: `[View Active Run](${runUrl})\n\nPR: #${{ needs.setup.outputs.pr_number }}\nHead SHA: ${{ needs.setup.outputs.head_sha }}` + } }); core.setOutput('check_id', check.data.id); - - name: Check out fork PR code + - name: Check out external PR code uses: actions/checkout@v6 with: repository: ${{ needs.setup.outputs.head_repo }} ref: ${{ needs.setup.outputs.head_sha }} + persist-credentials: false - name: Run service tests uses: ./.github/actions/run-service-tests @@ -121,8 +136,9 @@ jobs: completed_at: new Date().toISOString(), details_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, output: { - title: 'Service Tests Passed', - summary: `All service tests passed for PR #${{ inputs.pr_number }}.` + title: 'External PR Service Tests Passed', + summary: `All service tests passed for PR #${{ needs.setup.outputs.pr_number }}.`, + text: `[View Active Run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n\nPR: #${{ needs.setup.outputs.pr_number }}\nHead SHA: ${{ needs.setup.outputs.head_sha }}` } }); @@ -140,8 +156,8 @@ jobs: completed_at: new Date().toISOString(), details_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, output: { - title: 'Service Tests Failed', - summary: `The service tests failed for PR #${{ inputs.pr_number }}. Click "Details" to view the full test output and logs.`, - text: `**Workflow Run:** [View Details](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n\n**PR:** #${{ inputs.pr_number }}\n**Commit:** ${{ needs.setup.outputs.head_sha }}` + title: 'External PR Service Tests Failed', + summary: `The service tests failed for PR #${{ needs.setup.outputs.pr_number }}. Click "Details" to view the full test output and logs.`, + text: `[View Active Run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n\nPR: #${{ needs.setup.outputs.pr_number }}\nHead SHA: ${{ needs.setup.outputs.head_sha }}` } }); diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e4390486..4ae1f538f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: service-tests: name: Service Tests runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]') steps: - name: Check out repository uses: actions/checkout@v6 From 3a968fb0763d7b8c2f43aa352b92b44c89161201 Mon Sep 17 00:00:00 2001 From: Vishal Bala Date: Mon, 20 Apr 2026 15:15:19 +0200 Subject: [PATCH 2/4] Handle skipped `service-tests` for `test` [skip ci] --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4ae1f538f..e7c6a92e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -57,6 +57,7 @@ jobs: name: Python ${{ matrix.python-version }} - redis-py ${{ matrix.redis-py-version }} [${{ matrix.redis-image }}] runs-on: ubuntu-latest needs: service-tests + if: ${{ always() && needs.service-tests.result != 'failure' && needs.service-tests.result != 'cancelled' }} env: HF_HOME: ${{ github.workspace }}/hf_cache strategy: From b6d626f0ea27fc72197549fd8a90dee8d9b3851a Mon Sep 17 00:00:00 2001 From: Vishal Bala Date: Tue, 21 Apr 2026 09:39:27 +0200 Subject: [PATCH 3/4] Merge linting with test workflow [skip ci] --- .github/workflows/lint.yml | 65 -------------------------------------- .github/workflows/test.yml | 52 ++++++++++++++++++++++++++++-- 2 files changed, 50 insertions(+), 67 deletions(-) delete mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml deleted file mode 100644 index a28dc52b5..000000000 --- a/.github/workflows/lint.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: Lint - -on: - pull_request: - push: - branches: - - main - -env: - UV_VERSION: "0.7.13" - -jobs: - check: - name: Style-check ${{ matrix.python-version }} - runs-on: ubuntu-latest - strategy: - matrix: - # Only lint on the min and max supported Python versions. - # It's extremely unlikely that there's a lint issue on any version in between - # that doesn't show up on the min or max versions. - # - # GitHub rate-limits how many jobs can be running at any one time. - # Starting new jobs is also relatively slow, - # so linting on fewer versions makes CI faster. - python-version: - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" - - steps: - - name: Check out repository - uses: actions/checkout@v6 - - - name: Install Python - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Install uv - uses: astral-sh/setup-uv@v6 - with: - version: ${{ env.UV_VERSION }} - enable-cache: true - python-version: ${{ matrix.python-version }} # sets UV_PYTHON - cache-dependency-glob: | - pyproject.toml - uv.lock - - - name: Install dependencies - run: | - uv sync --all-extras --frozen - - - name: check-sort-import - run: | - make check-sort-imports - - - name: check-black-format - run: | - make check-format - - - name: check-mypy - run: | - make check-types \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e7c6a92e6..af56f9c81 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,9 +17,58 @@ env: UV_VERSION: "0.7.13" jobs: + lint: + name: Lint ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" + - "3.14" + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Install Python + uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + version: ${{ env.UV_VERSION }} + enable-cache: true + python-version: ${{ matrix.python-version }} # sets UV_PYTHON + cache-dependency-glob: | + pyproject.toml + uv.lock + + - name: Install dependencies + run: | + uv sync --all-extras --frozen + + - name: check-sort-import + run: | + make check-sort-imports + + - name: check-black-format + run: | + make check-format + + - name: check-mypy + run: | + make check-types + service-tests: name: Service Tests runs-on: ubuntu-latest + needs: lint if: github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.event.pull_request.user.login != 'dependabot[bot]') steps: - name: Check out repository @@ -56,8 +105,7 @@ jobs: test: name: Python ${{ matrix.python-version }} - redis-py ${{ matrix.redis-py-version }} [${{ matrix.redis-image }}] runs-on: ubuntu-latest - needs: service-tests - if: ${{ always() && needs.service-tests.result != 'failure' && needs.service-tests.result != 'cancelled' }} + needs: lint env: HF_HOME: ${{ github.workspace }}/hf_cache strategy: From 2b393a30a79dd2588b3c9fc0f21a6071a4f8c80f Mon Sep 17 00:00:00 2001 From: Vishal Bala Date: Sat, 9 May 2026 00:04:05 +0200 Subject: [PATCH 4/4] fix(ci): harden external PR workflow execution --- .github/workflows/test-fork-pr.yml | 200 +++++++++++++++++++++-------- .github/workflows/test.yml | 22 +++- 2 files changed, 169 insertions(+), 53 deletions(-) diff --git a/.github/workflows/test-fork-pr.yml b/.github/workflows/test-fork-pr.yml index 2283a67c4..35fb845dc 100644 --- a/.github/workflows/test-fork-pr.yml +++ b/.github/workflows/test-fork-pr.yml @@ -7,6 +7,10 @@ on: description: 'Pull Request number to test' required: true type: number + expected_head_sha: + description: 'Expected PR head SHA to run' + required: true + type: string permissions: contents: read @@ -29,8 +33,15 @@ jobs: - name: Get PR information id: pr-info uses: actions/github-script@v7 + env: + EXPECTED_HEAD_SHA: ${{ inputs.expected_head_sha }} with: script: | + const expectedHeadSha = (process.env.EXPECTED_HEAD_SHA || '').trim(); + if (!/^[0-9a-f]{40}$/i.test(expectedHeadSha)) { + core.setFailed(`Expected head SHA must be a 40-character commit SHA. Received: ${expectedHeadSha}`); + return; + } const pr = await github.rest.pulls.get({ owner: context.repo.owner, repo: context.repo.repo, @@ -42,6 +53,13 @@ jobs: return; } + if (pr.data.head.sha !== expectedHeadSha) { + core.setFailed( + `PR #${{ inputs.pr_number }} head SHA changed. Expected ${expectedHeadSha}, found ${pr.data.head.sha}.` + ); + return; + } + const isForkPr = pr.data.head.repo.full_name !== `${context.repo.owner}/${context.repo.repo}`; const isDependabotPr = pr.data.user.login === 'dependabot[bot]'; @@ -52,6 +70,40 @@ jobs: return; } + if (isDependabotPr) { + if (isForkPr || !pr.data.head.ref.startsWith('dependabot/')) { + core.setFailed( + `PR #${{ inputs.pr_number }} is not a standard same-repo Dependabot branch.` + ); + return; + } + + const allowedDependabotFiles = [ + /^pyproject\.toml$/, + /^uv\.lock$/, + /^requirements(?:\/.*|[^/]*)\.txt$/, + /^setup\.cfg$/, + ]; + const changedFiles = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: ${{ inputs.pr_number }}, + per_page: 100, + }); + const disallowedFiles = changedFiles + .map((file) => file.filename) + .filter( + (filename) => !allowedDependabotFiles.some((pattern) => pattern.test(filename)) + ); + + if (disallowedFiles.length > 0) { + core.setFailed( + `PR #${{ inputs.pr_number }} contains non-dependency file changes for a Dependabot run: ${disallowedFiles.slice(0, 10).join(', ')}` + ); + return; + } + } + core.setOutput('pr_number', String(pr.data.number)); core.setOutput('head_sha', pr.data.head.sha); core.setOutput('head_repo', pr.data.head.repo.full_name); @@ -64,6 +116,32 @@ jobs: name: Service Tests runs-on: ubuntu-latest needs: setup + timeout-minutes: 45 + concurrency: + group: external-pr-service-tests-${{ needs.setup.outputs.pr_number }} + cancel-in-progress: true + env: + HF_HOME: ${{ github.workspace }}/hf_cache + HF_TOKEN: ${{ secrets.HF_TOKEN }} + GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + GCP_LOCATION: ${{ secrets.GCP_LOCATION }} + GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} + COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} + MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} + VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} + AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} + AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} + OPENAI_API_VERSION: ${{ secrets.OPENAI_API_VERSION }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + LANGCACHE_WITH_ATTRIBUTES_API_KEY: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_API_KEY }} + LANGCACHE_WITH_ATTRIBUTES_CACHE_ID: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_CACHE_ID }} + LANGCACHE_WITH_ATTRIBUTES_URL: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_URL }} + LANGCACHE_NO_ATTRIBUTES_API_KEY: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_API_KEY }} + LANGCACHE_NO_ATTRIBUTES_CACHE_ID: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_CACHE_ID }} + LANGCACHE_NO_ATTRIBUTES_URL: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_URL }} steps: - name: Create check run id: check @@ -94,70 +172,92 @@ jobs: ref: ${{ needs.setup.outputs.head_sha }} persist-credentials: false - - name: Run service tests - uses: ./.github/actions/run-service-tests + - name: Cache HuggingFace Models + uses: actions/cache@v5 + with: + path: hf_cache + key: ${{ runner.os }}-hf-cache + + - name: Set HuggingFace token + shell: bash + run: | + mkdir -p ~/.huggingface + echo '{"token":"$HF_TOKEN"}' > ~/.huggingface/token + + - name: Install Python + uses: actions/setup-python@v6 with: python-version: ${{ env.PYTHON_VERSION }} - uv-version: ${{ env.UV_VERSION }} - env: - HF_HOME: ${{ github.workspace }}/hf_cache - HF_TOKEN: ${{ secrets.HF_TOKEN }} - GOOGLE_CREDENTIALS: ${{ secrets.GOOGLE_CREDENTIALS }} - OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} - GCP_LOCATION: ${{ secrets.GCP_LOCATION }} - GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} - COHERE_API_KEY: ${{ secrets.COHERE_API_KEY }} - MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }} - VOYAGE_API_KEY: ${{ secrets.VOYAGE_API_KEY }} - AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }} - AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }} - AZURE_OPENAI_DEPLOYMENT_NAME: ${{ secrets.AZURE_OPENAI_DEPLOYMENT_NAME }} - OPENAI_API_VERSION: ${{ secrets.OPENAI_API_VERSION }} - AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - LANGCACHE_WITH_ATTRIBUTES_API_KEY: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_API_KEY }} - LANGCACHE_WITH_ATTRIBUTES_CACHE_ID: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_CACHE_ID }} - LANGCACHE_WITH_ATTRIBUTES_URL: ${{ secrets.LANGCACHE_WITH_ATTRIBUTES_URL }} - LANGCACHE_NO_ATTRIBUTES_API_KEY: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_API_KEY }} - LANGCACHE_NO_ATTRIBUTES_CACHE_ID: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_CACHE_ID }} - LANGCACHE_NO_ATTRIBUTES_URL: ${{ secrets.LANGCACHE_NO_ATTRIBUTES_URL }} - - - name: Update check run (success) - if: success() - uses: actions/github-script@v7 + + - name: Install uv + uses: astral-sh/setup-uv@v6 with: - script: | - await github.rest.checks.update({ - owner: context.repo.owner, - repo: context.repo.repo, - check_run_id: ${{ steps.check.outputs.check_id }}, - status: 'completed', - conclusion: 'success', - completed_at: new Date().toISOString(), - details_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, - output: { - title: 'External PR Service Tests Passed', - summary: `All service tests passed for PR #${{ needs.setup.outputs.pr_number }}.`, - text: `[View Active Run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n\nPR: #${{ needs.setup.outputs.pr_number }}\nHead SHA: ${{ needs.setup.outputs.head_sha }}` - } - }); + version: ${{ env.UV_VERSION }} + enable-cache: true + python-version: ${{ env.PYTHON_VERSION }} + cache-dependency-glob: | + pyproject.toml + uv.lock + + - name: Install required dependencies + shell: bash + run: | + uv sync + + - name: Test module imports + shell: bash + run: | + uv run python -m tests.test_imports redisvl - - name: Update check run (failure) - if: failure() + - name: Install all dependencies + shell: bash + run: | + uv sync --all-extras + + - name: Authenticate to Google Cloud + uses: google-github-actions/auth@v1 + with: + credentials_json: ${{ env.GOOGLE_CREDENTIALS }} + + - name: Run full test suite + shell: bash + run: | + make test-all + + - name: Finalize check run + if: ${{ always() }} uses: actions/github-script@v7 with: script: | + const status = '${{ job.status }}'; + const conclusion = + status === 'success' ? 'success' : + status === 'cancelled' ? 'cancelled' : + 'failure'; + const title = + conclusion === 'success' ? 'External PR Service Tests Passed' : + conclusion === 'cancelled' ? 'External PR Service Tests Cancelled' : + 'External PR Service Tests Failed'; + const summary = + conclusion === 'success' + ? `All service tests passed for PR #${{ needs.setup.outputs.pr_number }}.` + : conclusion === 'cancelled' + ? `The external PR service tests were cancelled for PR #${{ needs.setup.outputs.pr_number }}.` + : `The service tests failed for PR #${{ needs.setup.outputs.pr_number }}. Click "Details" to view the full test output and logs.`; + const text = + `[View Active Run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n\nPR: #${{ needs.setup.outputs.pr_number }}\nHead SHA: ${{ needs.setup.outputs.head_sha }}`; + await github.rest.checks.update({ owner: context.repo.owner, repo: context.repo.repo, check_run_id: ${{ steps.check.outputs.check_id }}, status: 'completed', - conclusion: 'failure', + conclusion, completed_at: new Date().toISOString(), details_url: `https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`, output: { - title: 'External PR Service Tests Failed', - summary: `The service tests failed for PR #${{ needs.setup.outputs.pr_number }}. Click "Details" to view the full test output and logs.`, - text: `[View Active Run](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})\n\nPR: #${{ needs.setup.outputs.pr_number }}\nHead SHA: ${{ needs.setup.outputs.head_sha }}` + title, + summary, + text } }); diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c88a7645c..b3d21a408 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,13 +12,17 @@ on: workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + env: PYTHON_VERSION: "3.11" UV_VERSION: "0.7.13" jobs: lint: - name: Lint ${{ matrix.python-version }} + name: Style-check ${{ matrix.python-version }} runs-on: ubuntu-latest strategy: matrix: @@ -168,7 +172,13 @@ jobs: matrix.redis-py-version == '7.x' && matrix.redis-image == 'redis:8.4' && matrix.python-version == '3.11' && - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + ( + github.event_name != 'pull_request' || + ( + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' + ) + ) ) with: credentials_json: ${{ secrets.GOOGLE_CREDENTIALS }} @@ -178,7 +188,13 @@ jobs: matrix.redis-py-version == '7.x' && matrix.redis-image == 'redis:8.4' && matrix.python-version == '3.11' && - (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + ( + github.event_name != 'pull_request' || + ( + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' + ) + ) ) env: OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}