Skip to content
Latchkey

CI workflow (scverse/scanpy)

The CI workflow from scverse/scanpy, explained and optimized by Latchkey.

F

CI health: F - at risk

Point runs-on at Latchkey and get caching, run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.

Grade your own workflow free or run it on Latchkey →
Source: scverse/scanpy.github/workflows/ci.ymlLicense BSD-3-ClauseView source

What it does

This is the CI workflow from the scverse/scanpy repository, a real project running GitHub Actions. It is shown here with attribution under its BSD-3-Clause license.

Below, Latchkey shows a faster, safer version produced by its optimization engine.

The workflow

workflow (.yml)
name: CI

on:
  push:
    branches:
      - main
      - "[0-9]+.[0-9]+.x"
  pull_request:

env:
  PYTEST_ADDOPTS: "-v --color=yes -n auto --internet-tests --junitxml=test-data/test-results.xml"
  FORCE_COLOR: "1"
  MPLBACKEND: agg
  # It’s impossible to ignore SyntaxWarnings for a single module,
  # so because leidenalg 0.10.0 has them, we pre-compile things: https://github.com/vtraag/leidenalg/issues/173
  UV_COMPILE_BYTECODE: "1"

jobs:
  get-environments:
    runs-on: ubuntu-latest
    outputs:
      envs: ${{ steps.get-envs.outputs.envs }}
    steps:
      - uses: actions/checkout@v7
        with: { filter: 'blob:none', fetch-depth: 0 }
      - uses: astral-sh/setup-uv@v8.3.2
        with: { enable-cache: false }
      - id: get-envs
        run: |
          ENVS_JSON=$(NO_COLOR=1 uvx hatch env show --json | jq -c 'to_entries
            | map(
              select(.key | startswith("hatch-test"))
              | {
                name: .key,
                "test-type": (if (.key | test("pre|low-vers")) then "coverage" else null end),
                python: .value.python
              }
            )')
          echo "envs=${ENVS_JSON}" | tee $GITHUB_OUTPUT

  test:
    needs: get-environments
    runs-on: ubuntu-latest
    strategy:
      matrix:
        env: ${{ fromJSON(needs.get-environments.outputs.envs) }}
    permissions:
      id-token: write  # for codecov OIDC
    steps:
      - uses: actions/checkout@v7
        with: { filter: 'blob:none', fetch-depth: 0 }

      - uses: astral-sh/setup-uv@v8.3.2
        with:
          python-version: ${{ matrix.env.python }}

      - name: Cache downloaded data
        uses: actions/cache@v6
        with:
          path: .pytest_cache/d/scanpy-data
          key: pytest

      - name: Install dependencies
        run: |
          echo "::group::Install hatch"
          uv tool install hatch
          echo "::endgroup::"
          echo "::group::Create environment"
          hatch -v env create ${{ matrix.env.name }}
          echo "::endgroup::"
          hatch run ${{ matrix.env.name }}:session-info scanpy anndata

      - name: Run tests
        if: matrix.env.test-type == null
        run: hatch run ${{ matrix.env.name }}:run
      - name: Run tests (coverage)
        if: matrix.env.test-type == 'coverage'
        run: |
          hatch run ${{ matrix.env.name }}:run-cov
          hatch run ${{ matrix.env.name }}:cov-combine
          hatch run ${{ matrix.env.name }}:coverage xml
          hatch run ${{ matrix.env.name }}:cov-report

      - name: Upload coverage data
        if: ${{ !cancelled() && matrix.env.test-type == 'coverage' }}
        uses: codecov/codecov-action@v6
        with:
          flags: ${{ matrix.env.name }}
          files: test-data/coverage.xml
          use_oidc: true
          fail_ci_if_error: true

      - name: Upload test results
        if: ${{ !cancelled() }}
        uses: codecov/codecov-action@v6
        with:
          report_type: test_results
          flags: ${{ matrix.env.name }}
          files: test-data/test-results.xml
          use_oidc: true
          fail_ci_if_error: true

      - name: Publish debug artifacts
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v5
        with:
          name: debug-data-${{ matrix.env.name }}
          path: .pytest_cache/d/debug

  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
        with: { filter: 'blob:none', fetch-depth: 0 }
      - uses: actions/setup-python@v6
        with:
          python-version: "3.x"
      - uses: astral-sh/setup-uv@v8.3.2
        with:
          enable-cache: true
      - run: uvx --from build pyproject-build --sdist --wheel .
      - run: uvx twine check dist/*

  check:
    if: always()
    needs:
      - get-environments
      - test
      - build
    runs-on: ubuntu-latest
    steps:
      - uses: re-actors/alls-green@release/v1
        with:
          jobs: ${{ toJSON(needs) }}

The same workflow, on Latchkey

Estimated ~20% faster on cache hits, plus fewer wasted runs and a safer supply chain. Added and changed lines are highlighted.

name: CI
 
on:
  push:
    branches:
      - main
      - "[0-9]+.[0-9]+.x"
  pull_request:
 
env:
  PYTEST_ADDOPTS: "-v --color=yes -n auto --internet-tests --junitxml=test-data/test-results.xml"
  FORCE_COLOR: "1"
  MPLBACKEND: agg
  # It’s impossible to ignore SyntaxWarnings for a single module,
  # so because leidenalg 0.10.0 has them, we pre-compile things: https://github.com/vtraag/leidenalg/issues/173
  UV_COMPILE_BYTECODE: "1"
 
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
 
jobs:
  get-environments:
    timeout-minutes: 30
    runs-on: latchkey-small
    outputs:
      envs: ${{ steps.get-envs.outputs.envs }}
    steps:
      - uses: actions/checkout@v7
        with: { filter: 'blob:none', fetch-depth: 0 }
      - uses: astral-sh/setup-uv@v8.3.2
        with: { enable-cache: false }
      - id: get-envs
        run: |
          ENVS_JSON=$(NO_COLOR=1 uvx hatch env show --json | jq -c 'to_entries
            | map(
              select(.key | startswith("hatch-test"))
              | {
                name: .key,
                "test-type": (if (.key | test("pre|low-vers")) then "coverage" else null end),
                python: .value.python
              }
            )')
          echo "envs=${ENVS_JSON}" | tee $GITHUB_OUTPUT
 
  test:
    timeout-minutes: 30
    needs: get-environments
    runs-on: latchkey-small
    strategy:
      matrix:
        env: ${{ fromJSON(needs.get-environments.outputs.envs) }}
    permissions:
      id-token: write  # for codecov OIDC
    steps:
      - uses: actions/checkout@v7
        with: { filter: 'blob:none', fetch-depth: 0 }
 
      - uses: astral-sh/setup-uv@v8.3.2
        with:
          python-version: ${{ matrix.env.python }}
 
      - name: Cache downloaded data
        uses: actions/cache@v6
        with:
          path: .pytest_cache/d/scanpy-data
          key: pytest
 
      - name: Install dependencies
        run: |
          echo "::group::Install hatch"
          uv tool install hatch
          echo "::endgroup::"
          echo "::group::Create environment"
          hatch -v env create ${{ matrix.env.name }}
          echo "::endgroup::"
          hatch run ${{ matrix.env.name }}:session-info scanpy anndata
 
      - name: Run tests
        if: matrix.env.test-type == null
        run: hatch run ${{ matrix.env.name }}:run
      - name: Run tests (coverage)
        if: matrix.env.test-type == 'coverage'
        run: |
          hatch run ${{ matrix.env.name }}:run-cov
          hatch run ${{ matrix.env.name }}:cov-combine
          hatch run ${{ matrix.env.name }}:coverage xml
          hatch run ${{ matrix.env.name }}:cov-report
 
      - name: Upload coverage data
        if: ${{ !cancelled() && matrix.env.test-type == 'coverage' }}
        uses: codecov/codecov-action@v6
        with:
          flags: ${{ matrix.env.name }}
          files: test-data/coverage.xml
          use_oidc: true
          fail_ci_if_error: true
 
      - name: Upload test results
        if: ${{ !cancelled() }}
        uses: codecov/codecov-action@v6
        with:
          report_type: test_results
          flags: ${{ matrix.env.name }}
          files: test-data/test-results.xml
          use_oidc: true
          fail_ci_if_error: true
 
      - name: Publish debug artifacts
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v5
        with:
          name: debug-data-${{ matrix.env.name }}
          path: .pytest_cache/d/debug
 
  build:
    timeout-minutes: 30
    runs-on: latchkey-small
    steps:
      - uses: actions/checkout@v7
        with: { filter: 'blob:none', fetch-depth: 0 }
      - uses: actions/setup-python@v6
        with:
          cache: 'pip'
          python-version: "3.x"
      - uses: astral-sh/setup-uv@v8.3.2
        with:
          enable-cache: true
      - run: uvx --from build pyproject-build --sdist --wheel .
      - run: uvx twine check dist/*
 
  check:
    timeout-minutes: 30
    if: always()
    needs:
      - get-environments
      - test
      - build
    runs-on: latchkey-small
    steps:
      - uses: re-actors/alls-green@release/v1
        with:
          jobs: ${{ toJSON(needs) }}
 

What changed

3 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.

This workflow runs 4 jobs per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.

Actions used in this workflow