Skip to content

backend - with test coverage #442

backend - with test coverage

backend - with test coverage #442

name: backend - with test coverage
on:
workflow_dispatch:
workflow_call:
# Every 30 minutes
schedule:
- cron: '0,30 * * * *'
jobs:
# Skip generating coverage if already exists in GCS
check-existing-coverage:
name: check for existing coverage
runs-on: ubuntu-24.04
timeout-minutes: 5
permissions:
contents: read
id-token: write
outputs:
has-coverage: ${{ steps.check-coverage.outputs.exists }}
commit-sha: ${{ steps.get-sha.outputs.sha }}
steps:
- name: Determine commit SHA
id: get-sha
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
COMMIT_SHA="${{ github.event.pull_request.head.sha }}"
else
COMMIT_SHA="${{ github.sha }}"
fi
echo "sha=${COMMIT_SHA}" >> "$GITHUB_OUTPUT"
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.3
with:
project_id: sentry-dev-tooling
workload_identity_provider: ${{ secrets.SENTRY_GCP_DEV_WORKLOAD_IDENTITY_POOL }}
service_account: ${{ secrets.COLLECT_TEST_DATA_SERVICE_ACCOUNT_EMAIL }}
- name: Check if coverage exists
id: check-coverage
env:
COMMIT_SHA: ${{ steps.get-sha.outputs.sha }}
run: |
if gcloud storage ls "gs://sentry-coverage-data/${COMMIT_SHA}/.coverage.combined" &>/dev/null; then
echo "Coverage already exists for commit ${COMMIT_SHA}, skipping test run"
echo "exists=true" >> "$GITHUB_OUTPUT"
else
echo "No coverage found for commit ${COMMIT_SHA}, will run tests"
echo "exists=false" >> "$GITHUB_OUTPUT"
fi
calculate-shards:
name: calculate test shards
if: needs.check-existing-coverage.outputs.has-coverage != 'true'
needs: [check-existing-coverage]
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
shard-count: ${{ steps.calculate-shards.outputs.shard-count }}
shard-indices: ${{ steps.calculate-shards.outputs.shard-indices }}
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup sentry env
uses: ./.github/actions/setup-sentry
id: setup
with:
mode: backend-ci
skip-devservices: true
- name: Calculate test shards
id: calculate-shards
run: python3 .github/workflows/scripts/calculate-backend-test-shards.py
backend-test-with-cov-context:
name: backend test
if: needs.check-existing-coverage.outputs.has-coverage != 'true'
runs-on: ubuntu-24.04
needs: [check-existing-coverage, calculate-shards]
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
instance: ${{ fromJSON(needs.calculate-shards.outputs.shard-indices) }}
env:
MATRIX_INSTANCE_TOTAL: ${{ needs.calculate-shards.outputs.shard-count }}
TEST_GROUP_STRATEGY: roundrobin
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- name: Setup sentry env
uses: ./.github/actions/setup-sentry
id: setup
with:
mode: backend-ci
- name: Run backend test with coverage (${{ steps.setup.outputs.matrix-instance-number }} of ${{ steps.setup.outputs.matrix-instance-total }})
run: make test-backend-ci-with-coverage
- name: Validate coverage database
if: always()
run: |
set -euxo pipefail
if [[ ! -f .coverage ]]; then
echo "Error: No .coverage file found after tests"
exit 1
fi
python -c "import sqlite3; sqlite3.connect('.coverage').execute('SELECT COUNT(*) FROM file')"
- name: Upload raw coverage sqlite as artifact
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: pycoverage-sqlite-${{ github.run_id }}-${{ steps.setup.outputs.matrix-instance-number }}
path: .coverage
if-no-files-found: error
retention-days: 7
include-hidden-files: true
combine-coverage:
name: combine coverage
# Only upload coverage if all test shards pass - incomplete coverage could cause selective testing to skip tests incorrectly
if: needs.check-existing-coverage.outputs.has-coverage != 'true'
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
actions: read # used for DIM metadata
needs: [check-existing-coverage, backend-test-with-cov-context, calculate-shards]
steps:
- uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
- uses: astral-sh/setup-uv@884ad927a57e558e7a70b92f2bccf9198a4be546 # v6
with:
version: '0.8.2'
enable-cache: false
- name: Download all coverage artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
pattern: pycoverage-sqlite-${{ github.run_id }}-*
path: .artifacts/all-coverage
- name: Verify all shards produced coverage
env:
EXPECTED_SHARD_COUNT: ${{ needs.calculate-shards.outputs.shard-count }}
run: |
set -euxo pipefail
echo "Downloaded coverage artifacts:"
ls -la .artifacts/all-coverage || true
COVERAGE_FILE_COUNT=$(find .artifacts/all-coverage -name ".coverage" -type f | wc -l)
echo "Found ${COVERAGE_FILE_COUNT} coverage files, expected ${EXPECTED_SHARD_COUNT}"
if [[ "$COVERAGE_FILE_COUNT" -ne "$EXPECTED_SHARD_COUNT" ]]; then
echo "Error: Missing coverage files. Expected ${EXPECTED_SHARD_COUNT}, found ${COVERAGE_FILE_COUNT}"
echo "This indicates some test shards failed to produce coverage."
find .artifacts/all-coverage -name ".coverage" -type f
exit 1
fi
- name: Combine all coverage databases
run: |
set -euxo pipefail
uvx --with covdefaults --with sentry-covdefaults-disable-branch-coverage \
coverage combine $(find .artifacts/all-coverage -name ".coverage" -type f)
if [[ ! -f .coverage ]]; then
echo "Error: Combined coverage file was not created"
exit 1
fi
mv .coverage ".coverage.combined"
- name: Authenticate to Google Cloud
id: gcloud-auth
uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2.1.3
with:
project_id: sentry-dev-tooling
workload_identity_provider: ${{ secrets.SENTRY_GCP_DEV_WORKLOAD_IDENTITY_POOL }}
service_account: ${{ secrets.COLLECT_TEST_DATA_SERVICE_ACCOUNT_EMAIL }}
- name: Upload coverage to GCS
uses: google-github-actions/upload-cloud-storage@c0f6160ff80057923ff50e5e567695cea181ec23 # v2.2.4
with:
path: .coverage.combined
destination: sentry-coverage-data/${{ needs.check-existing-coverage.outputs.commit-sha }}