GitHub Actions CI Workflow Builder (Python)

Generate a Python CI workflow with pytest, flake8, and matrix tests

Build a GitHub Actions CI workflow for Python with pip dependency caching, a multi-version test matrix, pytest with a coverage report and artifact upload, flake8 linting, and an optional Docker build job. Copy a ready-to-commit workflow built in your browser. It runs free in your browser on Gera Tools, with nothing uploaded.

Last updated Source: Gera Tools

Where does this workflow file go?

Save it as .github/workflows/ci.yml in your repository root. GitHub runs any workflow under .github/workflows automatically on the configured events, here push and pull_request to your chosen branch.

A GitHub Actions CI workflow builder for Python that generates a valid .github/workflows/ci.yml. Choose the Python versions to test, whether to lint with flake8, whether to collect coverage, and whether to add a Docker build, and it produces a matrix workflow with pip caching.

How it works

The test job runs on ubuntu-latest with a strategy.matrix.python-version listing your versions as quoted strings (so 3.10 is not parsed as the number 3.1). fail-fast: false runs every version even when one fails. Each run checks out the code, sets up Python with actions/setup-python@v5 and cache: pip, then installs requirements.txt plus pytest and the optional tooling.

When lint is on, it runs the two-pass flake8 pattern: a strict pass that fails on real errors (E9,F63,F7,F82) and a permissive style pass. When coverage is on, pytest runs with --cov and the resulting coverage.xml is uploaded as a per-version artifact. An optional second docker job, gated by needs: test, builds your image tagged with the commit SHA only after the matrix passes.

A generated workflow, annotated

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: ["3.10", "3.11", "3.12"]  # quoted to prevent YAML float parse
      fail-fast: false  # run all versions even if one fails

    steps:
      - uses: actions/checkout@v4

      - name: Set up Python ${{ matrix.python-version }}
        uses: actions/setup-python@v5
        with:
          python-version: ${{ matrix.python-version }}
          cache: pip  # caches pip's download directory keyed on requirements.txt

      - name: Install dependencies
        run: pip install -r requirements.txt pytest pytest-cov

      - name: Lint (strict pass — blocks merge)
        run: flake8 . --select=E9,F63,F7,F82 --show-source

      - name: Lint (style pass — advisory)
        run: flake8 . --max-line-length=127 --exit-zero

      - name: Test with coverage
        run: pytest --cov --cov-report=xml

      - name: Upload coverage
        uses: actions/upload-artifact@v4
        with:
          name: coverage-${{ matrix.python-version }}
          path: coverage.xml

Understanding the two-pass flake8 pattern

The two flake8 calls serve different purposes:

  1. Strict pass (--select=E9,F63,F7,F82): catches genuine errors — syntax errors, undefined names, wildcard import failures, and runtime-undefined __all__ members. These would crash your code; the pass fails the build if any are found.
  2. Style pass (full ruleset, --exit-zero): reports PEP 8 style issues as advisory warnings without blocking the merge. Teams can tighten this over time by removing --exit-zero once the codebase is clean.

This pattern is recommended by the official GitHub Python starter workflow and avoids the trap of failing CI over trailing whitespace while letting genuine bugs through.

Tips and notes

  • Python versions are emitted as quoted strings on purpose — an unquoted 3.10 would be read as the float 3.1 by YAML, silently testing the wrong runtime.
  • Keep a requirements.txt (or adjust the install step for pyproject.toml / requirements-dev.txt); the workflow installs from it before running tests.
  • Replace the coverage upload with the codecov/codecov-action step if you want coverage trends and PR comments rather than a downloadable artifact.
  • The strict flake8 pass is the one that should gate merges — it flags undefined names and syntax errors that would crash at runtime, independent of style preferences.
  • Use ubuntu-latest rather than a pinned runner version; GitHub updates the label to the newest available LTS release over time.