diff --git a/.github/workflows/trigger-integration-tests.yml b/.github/workflows/trigger-integration-tests.yml new file mode 100644 index 000000000..5e599fb57 --- /dev/null +++ b/.github/workflows/trigger-integration-tests.yml @@ -0,0 +1,409 @@ +name: Trigger Integration Tests + +# Dispatches the proxy-based integration test suite in +# databricks/databricks-driver-test to run against this PR's commit. +# +# Mirrors the canonical pattern in adbc-drivers/databricks. The model: +# +# - On a normal PR event (open / push / reopen / non-IT label) we +# post a `success` Python Proxy Tests check immediately so the +# required check doesn't block the PR. The real tests are gated +# in the merge queue. +# - When a maintainer adds the `integration-test` label we dispatch +# the suite as a preview — useful for catching regressions before +# merge queue time. +# - Pushing new commits to the PR auto-removes the label so a +# subsequent labelled run requires a fresh maintainer review. +# - On the `merge_group` event the suite runs as the real required +# gate. Only PRs whose tests dispatch (or auto-pass when no driver +# files changed) can proceed to `main`. +# +# Required external setup (outside this workflow): +# +# 1. `integration-test` label exists in this repo (one-off; created +# separately). +# 2. `INTEGRATION_TEST_APP_ID` / `INTEGRATION_TEST_PRIVATE_KEY` repo +# secrets installed for the dispatcher GitHub App (write access +# to databricks/databricks-driver-test). +# 3. Merge queue enabled on `main` branch protection AND +# `Python Proxy Tests` listed as a required status check. Without +# this the merge-queue job is dead code and ITs run only on +# explicit label. + +on: + pull_request: + types: [opened, synchronize, reopened, labeled] + merge_group: # Trigger when added to merge queue + +jobs: + # ============================================================================= + # Security: Auto-remove label when new commits are pushed + # ============================================================================= + remove-label-on-new-commit: + if: github.event_name == 'pull_request' && github.event.action == 'synchronize' + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + permissions: + pull-requests: write + issues: write + steps: + - name: Check if integration-test label exists + id: check-label + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const labels = context.payload.pull_request.labels.map(l => l.name); + const hasLabel = labels.includes('integration-test'); + console.log(`integration-test label exists: ${hasLabel}`); + return hasLabel; + + - name: Remove integration-test label + if: steps.check-label.outputs.result == 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + try { + await github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + name: 'integration-test' + }); + console.log('Removed integration-test label'); + } catch (error) { + if (error.status === 404) { + console.log('Label already removed or does not exist'); + } else { + throw error; + } + } + + - name: Comment on PR about label removal + if: steps.check-label.outputs.result == 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const pr = context.payload.pull_request; + const isFromFork = pr.head.repo.full_name !== pr.base.repo.full_name; + const repoType = isFromFork ? '**fork PR**' : 'PR'; + + const body = [ + 'Integration test approval reset.', + '', + `New commits were pushed to this ${repoType}. The \`integration-test\` label has been automatically removed for security.`, + '', + '**A maintainer must re-review the changes and re-add the label to trigger tests again.**', + '', + `Latest commit: ${pr.head.sha.substring(0, 7)}` + ].join('\n'); + + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + + # ============================================================================= + # For PRs: Always pass the Python Proxy Tests check on non-label events. + # The real run happens in the merge queue (or via explicit label preview). + # Without this, a required `Python Proxy Tests` check would block every + # PR that doesn't bother labelling. + # ============================================================================= + skip-integration-tests-pr: + if: github.event_name == 'pull_request' && github.event.action != 'labeled' + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + permissions: + checks: write + steps: + - name: Skip Python Proxy Tests + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + github-token: ${{ github.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Python Proxy Tests', + head_sha: context.payload.pull_request.head.sha, + status: 'completed', + conclusion: 'success', + completed_at: new Date().toISOString(), + output: { + title: 'Skipped on PR — runs in merge queue', + summary: 'Python Proxy Tests are skipped on PRs and run as a required gate in the merge queue. Add the `integration-test` label to preview them on this PR.' + } + }); + + # ============================================================================= + # For PRs: Dispatch real tests when integration-test label is added. + # Only dispatches when driver source files changed; otherwise posts + # an auto-pass check so the gate isn't artificially red. + # ============================================================================= + trigger-tests-pr: + if: | + github.event_name == 'pull_request' && + github.event.action == 'labeled' && + contains(github.event.pull_request.labels.*.name, 'integration-test') + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + permissions: + issues: write + pull-requests: write + checks: write + steps: + - name: Detect changed driver paths + id: changed + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + const { data: files } = await github.rest.pulls.listFiles({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.payload.pull_request.number, + per_page: 100 + }); + const names = files.map(f => f.filename); + // Driver source + the workflow itself + pyproject.toml + // (dep bumps can break the integration suite). Anything + // under tests/unit/ doesn't need IT, but tests/e2e/ does. + const sourceChanged = names.some(f => + f.startsWith('src/') || + f.startsWith('tests/e2e/') || + f === 'pyproject.toml' || + f === 'poetry.lock' + ); + const workflowChanged = names.some(f => + f.startsWith('.github/workflows/') + ); + const runPython = sourceChanged || workflowChanged; + if (workflowChanged) console.log('Workflow files changed — triggering ITs'); + if (sourceChanged) console.log('Driver source files changed — triggering ITs'); + core.setOutput('python', runPython.toString()); + + - name: Generate GitHub App Token (internal repo) + id: app-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + with: + app-id: ${{ secrets.INTEGRATION_TEST_APP_ID }} + private-key: ${{ secrets.INTEGRATION_TEST_PRIVATE_KEY }} + owner: databricks + repositories: databricks-driver-test + + - name: Generate GitHub App Token (public repo) + id: public-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + with: + app-id: ${{ secrets.INTEGRATION_TEST_APP_ID }} + private-key: ${{ secrets.INTEGRATION_TEST_PRIVATE_KEY }} + owner: databricks + repositories: databricks-sql-python + + - name: Sanitize PR title + id: sanitize + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + result-encoding: string + script: | + // Remove characters that could break the dispatch JSON or + // enable injection of extra fields via a crafted title. + const title = context.payload.pull_request.title || ''; + return title.replace(/[\\"\n\r\t]/g, ' ').substring(0, 200); + + - name: Dispatch Python tests to internal repo + if: steps.changed.outputs.python == 'true' + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + with: + token: ${{ steps.app-token.outputs.token }} + repository: databricks/databricks-driver-test + event-type: python-pr-test + client-payload: | + { + "pr_number": "${{ github.event.pull_request.number }}", + "commit_sha": "${{ github.event.pull_request.head.sha }}", + "pr_repo": "${{ github.repository }}", + "pr_url": "${{ github.event.pull_request.html_url }}", + "pr_title": "${{ steps.sanitize.outputs.result }}", + "pr_author": "${{ github.event.pull_request.user.login }}" + } + + - name: Pass Python Proxy Tests check (no driver changes) + if: steps.changed.outputs.python != 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + github-token: ${{ steps.public-token.outputs.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Python Proxy Tests', + head_sha: context.payload.pull_request.head.sha, + status: 'completed', + conclusion: 'success', + completed_at: new Date().toISOString(), + output: { + title: 'Skipped — no driver changes', + summary: 'No Python driver source files changed; skipping integration tests.' + } + }); + + - name: Fail check on dispatch error + if: failure() && steps.changed.outputs.python == 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + github-token: ${{ steps.public-token.outputs.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Python Proxy Tests', + head_sha: context.payload.pull_request.head.sha, + status: 'completed', + conclusion: 'failure', + completed_at: new Date().toISOString(), + output: { + title: 'Failed — error dispatching tests', + summary: 'An error occurred while dispatching Python integration tests. Check the workflow run logs.' + } + }); + + - name: Comment on PR + if: steps.changed.outputs.python == 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + script: | + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: 'Integration tests triggered. [View workflow run](https://github.com/databricks/databricks-driver-test/actions/workflows/python-proxy-tests.yml).' + }); + + # ============================================================================= + # For Merge Queue: Real gate. Dispatch tests when driver files changed; + # otherwise auto-pass so the queue isn't blocked. + # ============================================================================= + merge-queue-python: + if: github.event_name == 'merge_group' + runs-on: + group: databricks-protected-runner-group + labels: linux-ubuntu-latest + permissions: + contents: read + checks: write + steps: + - name: Checkout code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + fetch-depth: 0 + + - name: Check if driver files changed + id: changed + env: + BASE_SHA: ${{ github.event.merge_group.base_sha }} + HEAD_SHA: ${{ github.event.merge_group.head_sha }} + run: | + CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA") + if echo "$CHANGED" | grep -qE "^(src/|tests/e2e/|pyproject\.toml|poetry\.lock|\.github/workflows/)"; then + echo "changed=true" >> "$GITHUB_OUTPUT" + echo "Driver files changed — will dispatch tests" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + echo "No driver files changed — will auto-pass" + fi + + - name: Generate GitHub App Token (public repo) + id: public-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + with: + app-id: ${{ secrets.INTEGRATION_TEST_APP_ID }} + private-key: ${{ secrets.INTEGRATION_TEST_PRIVATE_KEY }} + owner: databricks + repositories: databricks-sql-python + + - name: Auto-pass (no driver changes) + if: steps.changed.outputs.changed != 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + github-token: ${{ steps.public-token.outputs.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Python Proxy Tests', + head_sha: '${{ github.event.merge_group.head_sha }}', + status: 'completed', + conclusion: 'success', + completed_at: new Date().toISOString(), + output: { + title: 'Skipped — no driver changes', + summary: 'No Python driver source files changed.' + } + }); + + - name: Extract PR number from merge queue ref + if: steps.changed.outputs.changed == 'true' + id: extract-pr + env: + MERGE_QUEUE_REF: ${{ github.event.merge_group.head_ref }} + run: | + # GitHub names the queue branch as + # `gh-readonly-queue//pr--` — extract N so the + # dispatched payload links back to the originating PR. + if [[ "$MERGE_QUEUE_REF" =~ pr-([0-9]+) ]]; then + echo "pr_number=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" + else + echo "Error: failed to extract PR number from merge group ref: '$MERGE_QUEUE_REF'" >&2 + exit 1 + fi + + - name: Generate GitHub App Token (internal repo) + if: steps.changed.outputs.changed == 'true' + id: app-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + with: + app-id: ${{ secrets.INTEGRATION_TEST_APP_ID }} + private-key: ${{ secrets.INTEGRATION_TEST_PRIVATE_KEY }} + owner: databricks + repositories: databricks-driver-test + + - name: Dispatch Python tests + if: steps.changed.outputs.changed == 'true' + uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 + with: + token: ${{ steps.app-token.outputs.token }} + repository: databricks/databricks-driver-test + event-type: python-pr-test + client-payload: | + { + "pr_number": "${{ steps.extract-pr.outputs.pr_number }}", + "commit_sha": "${{ github.event.merge_group.head_sha }}", + "pr_repo": "${{ github.repository }}", + "pr_url": "${{ github.server_url }}/${{ github.repository }}/pull/${{ steps.extract-pr.outputs.pr_number }}", + "pr_title": "Merge queue validation", + "pr_author": "merge-queue" + } + + - name: Fail check on dispatch error + if: failure() && steps.changed.outputs.changed == 'true' + uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0 + with: + github-token: ${{ steps.public-token.outputs.token }} + script: | + await github.rest.checks.create({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'Python Proxy Tests', + head_sha: '${{ github.event.merge_group.head_sha }}', + status: 'completed', + conclusion: 'failure', + completed_at: new Date().toISOString(), + output: { + title: 'Failed — error dispatching tests', + summary: 'An error occurred while dispatching Python integration tests. Check the workflow run logs.' + } + });