Skip to content

Modernize packaging and tooling from the scverse template (#215) #2056

Modernize packaging and tooling from the scverse template (#215)

Modernize packaging and tooling from the scverse template (#215) #2056

Workflow file for this run

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) }}