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:
- 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. - 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-zeroonce 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.10would be read as the float3.1by YAML, silently testing the wrong runtime. - Keep a
requirements.txt(or adjust the install step forpyproject.toml/requirements-dev.txt); the workflow installs from it before running tests. - Replace the coverage upload with the
codecov/codecov-actionstep 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-latestrather than a pinned runner version; GitHub updates the label to the newest available LTS release over time.