When you ask AI to "set up CI for my FastAPI project," you get a workflow file in seconds. It will have the right shape, triggers, jobs, steps, but it will be missing the details that make CI actually useful. This lesson teaches you to read those workflows critically, spot what AI leaves out, and understand the infrastructure your pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. runs on.
Workflow files and where they live
Every GitHub Actions workflow is a YAMLWhat is yaml?A human-readable text format used for configuration files, including Docker Compose and GitHub Actions workflows. file inside .github/workflows/. GitHub scans this directory automatically, drop a valid YAML file in there, push it, and the pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. runs. No configuration UI, no third-party setup.
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- run: pip install -r requirements.txt
- run: pytestThis is the skeleton that AI generates. It works, but it is the bare minimum. Let's understand each piece and what is missing.
Trigger events
The on block defines when the workflow runs. The two most common triggers for CI are push (code lands on a branch) and pull_request (a PR is opened or updated).
on:
push:
branches: [main] # Runs when code is pushed to main
pull_request:
branches: [main] # Runs when a PR targets mainYou can also trigger on schedules (nightly builds), tags (release builds), or manual dispatch (one-off runs from the GitHub UI):
on:
schedule:
- cron: "0 6 * * 1" # Every Monday at 6 AM UTC
workflow_dispatch: # Manual trigger button in GitHub UIpush and pull_request triggers. For Python projects that depend on external packages, a weekly scheduled run catches upstream dependency breakage before it hits your developers on Monday morning.Python setup
The actions/setup-python@v5 action installs a specific Python version on the runner. This is where AI makes its first common mistake: hardcoding a single version or, worse, omitting the version entirely.
# What AI generates - works, but fragile
- uses: actions/setup-python@v5
with:
python-version: "3.11"
# What production needs - explicit version, pinned action
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"The cache: "pip" option is built into setup-python@v5. It caches the pip download cache based on your requirements.txt hash, so subsequent runs skip downloading packages that have not changed.
Why the Python version matters
Python 3.11, 3.12, and 3.13 have meaningful differences, new syntax features, performance improvements, and deprecated APIs. If your CI runs on 3.11 but your production server runs 3.12, you might miss deprecationWhat is deprecation?Marking a feature or API version as outdated and scheduled for removal, giving users time to switch to the replacement. warnings or use features that do not exist in your CI environment.
| Python version | Key changes relevant to CI |
|---|---|
| 3.11 | Exception groups, tomllib in stdlib |
| 3.12 | Improved error messages, type statement, f-string improvements |
| 3.13 | Experimental free-threaded mode, improved typing module |
pyproject.toml specifies. Always match your CI Python version to your production Python version, or better yet, use a matrix to test both.Pip caching
Without caching, every CI run downloads every dependencyWhat is dependency?A piece of code written by someone else that your project needs to work. Think of it as a building block you import instead of writing yourself. from PyPI from scratch. For a typical FastAPI project with 40-60 transitive dependencies, that is 30-90 seconds of pure download time on every push.
There are two caching approaches:
# Option 1: Built-in cache in setup-python (simpler)
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
# Option 2: Explicit cache action (more control)
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-Option 1 is simpler and covers most cases. Option 2 gives you control over the cache key, which matters when you have multiple requirements files (requirements.txt, requirements-dev.txt, requirements-test.txt).
Running linting, type checking, and tests
A complete CI pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production. for Python runs three categories of checks, in order from fastest to slowest:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: "pip"
- name: Install dependencies
run: |
pip install -r requirements.txt
pip install -r requirements-dev.txt
- name: Lint with ruff
run: ruff check .
- name: Type check with mypy
run: mypy src/
- name: Run tests
run: pytest| Step | Tool | What it catches | Speed |
|---|---|---|---|
| Lint | ruff | Style issues, unused imports, common bugs | Seconds |
| Type check | mypy | Type mismatches, missing attributes, wrong return types | Seconds to minutes |
| Test | pytest | Logic errors, regressions, broken integrations | Seconds to minutes |
The order matters. Linting is near-instant and catches obvious issues first. Type checking runs next and surfaces deeper problems. Tests run last because they are the slowest. If linting fails, there is no point waiting for tests.
Separate requirements files
Production dependencies and development dependencies should live in separate files:
requirements.txt # Production: fastapi, uvicorn, sqlalchemy, etc.
requirements-dev.txt # Development: pytest, ruff, mypy, etc.In CI, you install both. In production DockerWhat is docker?A tool that packages your application and all its dependencies into a portable container that runs identically on any machine. images, you install only requirements.txt. AI-generated workflows typically reference only requirements.txt, which means your linterWhat is linter?A tool that scans your code for style violations, common mistakes, and suspicious patterns without running it., type checker, and test runner are not installed, and the CI silently skips those steps or fails with a "command not found" error.
pip install -r requirements.txt and then runs pytest, but pytest is a dev dependency. The workflow either fails because pytest is not installed, or AI puts pytest in requirements.txt, shipping test dependencies to production.Pinning action versions
Actions are third-party code running in your pipelineWhat is pipeline?A sequence of automated steps (install, lint, test, build, deploy) that code passes through before reaching production.. Pinning to a major version (@v5) protects against breaking changes. Pinning to a commitWhat is commit?A permanent snapshot of your staged changes saved in Git's history, identified by a unique hash and accompanied by a message describing what changed. SHA protects against supply-chain attacks where a compromised maintainer pushes malicious code.
# Good - pinned to major version
- uses: actions/setup-python@v5
# Better - pinned to commit SHA
- uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b
# Dangerous - follows the branch, could change any time
- uses: actions/setup-python@mainFor most teams, major version pinning (@v5) is the right balance. SHA pinning is worth the effort for high-security environments.
Reading AI-generated workflows critically
Here is a checklist for reviewing any AI-generated GitHub Actions workflow for Python:
| Check | What to look for |
|---|---|
| Python version | Does it match your production version? |
| Pip caching | Is cache: "pip" set, or actions/cache@v4 configured? |
| Dev dependencies | Are requirements-dev.txt deps installed for lint/test steps? |
| Action versions | Are actions pinned to @v4/@v5, not @main? |
| Lint step | Is ruff (or flake8) running before tests? |
| Type check step | Is mypy included? |
| Trigger scope | Does it run on both push and pull_request? |
| Secrets | Are API keys using ${{ secrets.NAME }}, not hardcoded? |
Quick reference
| Element | Syntax | Purpose |
|---|---|---|
| Trigger | on: push, on: pull_request | When the workflow runs |
| Python setup | actions/setup-python@v5 | Install specific Python version |
| Pip cache | cache: "pip" in setup-python | Skip redundant downloads |
| Lint | ruff check . | Fast code quality check |
| Type check | mypy src/ | Catch type errors before runtime |
| Test | pytest | Verify correctness |
| Pin action | @v5 or @sha | Lock action version for security |