Modernize packaging and tooling from the scverse template (#215) #2056
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI - tests | |
| on: | |
| # Scheduled runs twice weekly: save the pytest output to a file, upload it as | |
| # an artifact, and commit the 3.12 report back to the branch. | |
| schedule: | |
| - cron: "0 16 * * 1,4" | |
| # Run post-merge on the integration branches only — pushes to a PR's feature | |
| # branch are already covered by the pull_request event below, so scoping push | |
| # to main/dev avoids running the suite twice for the same commit. | |
| push: | |
| branches: [main, dev] | |
| paths: | |
| - "gget/**" | |
| - "tests/**" | |
| - "pyproject.toml" | |
| # Avoid recursively triggering on the bot-committed pytest result files. | |
| - "!tests/pytest_results_py*.txt" | |
| # Run on every pull request into the integration branches. | |
| pull_request: | |
| branches: [main, dev] | |
| paths: | |
| - "gget/**" | |
| - "tests/**" | |
| - "pyproject.toml" | |
| # Manual runs behave like scheduled runs: | |
| # save output, upload artifact, and commit report back. | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| # Derive the test matrix from pyproject.toml ([tool.hatch.envs.hatch-test]), | |
| # so the tested environments are defined in a single place and stay identical | |
| # locally (`hatch test`) and in CI. | |
| get-environments: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| envs: ${{ steps.get-envs.outputs.envs }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| - name: Get test environments from hatch | |
| id: get-envs | |
| run: | | |
| ENVS_JSON=$(uvx hatch env show --json | jq -c 'to_entries | |
| | map(select(.key | startswith("hatch-test")) | { name: .key, python: .value.python })') | |
| echo "envs=${ENVS_JSON}" | tee "$GITHUB_OUTPUT" | |
| test: | |
| needs: get-environments | |
| name: ${{ matrix.env.name }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # commit pytest report back on scheduled/manual runs | |
| id-token: write # codecov OIDC | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| env: ${{ fromJSON(needs.get-environments.outputs.envs) }} | |
| steps: | |
| - name: Checkout branch | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v7 | |
| with: | |
| python-version: ${{ matrix.env.python }} | |
| # Builds the environment (project + test dependency-group, plus the | |
| # cellxgene extra only where pyproject says it is available). | |
| - name: Create hatch test environment | |
| run: uvx hatch env create ${{ matrix.env.name }} | |
| # Push/PR: run tests and fail the job immediately on test failure. | |
| - name: Run tests (push / pull_request) | |
| if: github.event_name == 'push' || github.event_name == 'pull_request' | |
| env: | |
| MPLBACKEND: agg | |
| run: uvx hatch run ${{ matrix.env.name }}:run-cov -ra -v --durations=10 | |
| # Scheduled/manual: save full output to a file and capture the real pytest | |
| # exit code. "set +e" keeps a test failure from preventing the exit-code / | |
| # artifact / report-commit handling below; continue-on-error does the same | |
| # at the step level. | |
| - name: Run tests and save output (schedule / workflow_dispatch) | |
| id: pytest_saved | |
| if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' | |
| continue-on-error: true | |
| shell: bash | |
| env: | |
| MPLBACKEND: agg | |
| run: | | |
| set -o pipefail | |
| OUT="tests/pytest_results_py${{ matrix.env.python }}.txt" | |
| echo "Pytest results (Python ${{ matrix.env.python }}) - $(date -u +"%Y-%m-%dT%H:%M:%SZ")" > "$OUT" | |
| echo "" >> "$OUT" | |
| set +e | |
| uvx hatch run ${{ matrix.env.name }}:run-cov -ra -v --durations=10 2>&1 | tee -a "$OUT" | |
| code=${PIPESTATUS[0]} | |
| set -e | |
| echo "exit_code=$code" >> "$GITHUB_OUTPUT" | |
| echo "pytest exit code: $code" | |
| exit 0 | |
| # Coverage upload is best-effort: a failure here must not mask the test | |
| # result (which is handled by the steps above/below). | |
| - name: Generate coverage report | |
| if: always() | |
| continue-on-error: true | |
| run: | | |
| test -f .coverage || uvx hatch run ${{ matrix.env.name }}:cov-combine | |
| uvx hatch run ${{ matrix.env.name }}:coverage xml | |
| - name: Upload coverage to Codecov | |
| if: always() | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| use_oidc: true | |
| fail_ci_if_error: false | |
| # Upload the saved pytest report as an artifact. | |
| # Only once (3.12) to avoid duplicate artifacts from the matrix. | |
| - name: Upload pytest results artifact | |
| if: always() && matrix.env.python == '3.12' && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: pytest-results-py${{ matrix.env.python }} | |
| path: tests/pytest_results_py${{ matrix.env.python }}.txt | |
| # Commit the saved pytest report back to the repository. | |
| # Safety guards: | |
| # - only on scheduled/manual runs | |
| # - only once (3.12) to avoid matrix push races | |
| # - commits back to the branch that the run was triggered from | |
| # Retry logic helps if the branch moved while this job was running. | |
| - name: Commit and push pytest results | |
| if: > | |
| always() && | |
| matrix.env.python == '3.12' && | |
| (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BRANCH="${GITHUB_REF#refs/heads/}" | |
| echo "Current branch: $BRANCH" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add tests/pytest_results_py*.txt | |
| if git diff --cached --quiet; then | |
| echo "No changes to commit." | |
| exit 0 | |
| fi | |
| git commit -m "CI: update pytest results ($BRANCH)" | |
| for attempt in 1 2 3 4 5; do | |
| echo "Push attempt $attempt..." | |
| git pull --rebase --autostash origin "$BRANCH" || true | |
| if git push origin "$BRANCH"; then | |
| exit 0 | |
| fi | |
| sleep $((attempt * 5)) | |
| done | |
| echo "Push failed after retries." | |
| exit 1 | |
| # After scheduled/manual runs, explicitly fail the job if pytest failed. | |
| # Separate so that artifact upload and report commit still happen on failure. | |
| - name: Fail job if pytest failed | |
| if: always() && (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch') | |
| shell: bash | |
| run: | | |
| code="${{ steps.pytest_saved.outputs.exit_code }}" | |
| echo "Captured pytest exit code: ${code:-<missing>}" | |
| if [ -z "${code:-}" ]; then | |
| echo "pytest exit code was not captured" | |
| exit 1 | |
| fi | |
| if [ "$code" != "0" ]; then | |
| exit "$code" | |
| fi | |
| # Single gate job so branch protection can require one stable check name | |
| # instead of every matrix entry. See https://github.com/re-actors/alls-green. | |
| check: | |
| name: Tests pass | |
| if: always() | |
| needs: | |
| - get-environments | |
| - test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: re-actors/alls-green@release/v1 | |
| with: | |
| jobs: ${{ toJSON(needs) }} |