Continuous Integration

Running tests and security checks automatically on every pull request

Concepts

Why automate?

A first workflow

name: CI

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uv sync
      - run: uv run task check
      - run: uv run pytest

Scanning for vulnerable dependencies

  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - run: uv sync
      - run: uv run pip-audit

Static security analysis

Reading the results

Check for Understanding

What is the difference between a CI job and a CI step?

A job is a collection of steps that run sequentially on the same virtual machine. Multiple jobs in a workflow run in parallel by default, each on their own machine, and each starts from a clean environment. A step is a single command or action within a job. Steps within a job share the same filesystem and environment variables; jobs do not share anything unless you explicitly pass artifacts between them.

Why run the linting check and the test suite as separate steps rather than one shell command?

When they are separate steps, GitHub shows each one as a named row with its own pass/fail indicator. If linting fails and tests are not run (because the step failed and subsequent steps are skipped by default), you know immediately that the problem is a linting violation, not a test failure. A single combined command produces a single pass/fail result, which gives you less information when something goes wrong.

What does pip-audit check, and what does it not check?

pip-audit checks the packages installed in the environment against a database of known vulnerabilities that have been assigned CVE identifiers. It does not check for bugs in your own code, insecure coding patterns, or vulnerabilities that have not yet been publicly disclosed. It is a necessary check, but not a sufficient one---which is why ruff's S rules and code review still matter.

If a CI check fails on a pull request, does that prevent the merge?

Not by default. GitHub shows the failed check on the pull request page, but still offers a "Merge" button unless you have configured branch protection rules to require the check to pass. Go to the repository settings, select "Branches", add a branch protection rule for main, and enable "Require status checks to pass before merging". Without that setting, CI is advisory, not mandatory.

Exercises

Require the check to pass

Configure branch protection rules on your repository so that the CI workflow must pass before a pull request can be merged into main. Open a pull request that deliberately breaks a test and confirm that GitHub blocks the merge. Fix the test, push again, and confirm the merge becomes available once CI passes.

Add a matrix build

Modify the workflow to run the test suite on both Python 3.12 and Python 3.13 in parallel using a build matrix. Ask the LLM to show you the strategy.matrix syntax. Read the generated YAML before accepting it and verify that both Python versions appear as separate jobs in the Actions tab.

Audit the current dependencies

Run uv run pip-audit locally and examine the output. If any vulnerabilities are reported, look up the CVE identifier for one of them and read its description. Write a one-paragraph summary of what the vulnerability is, whether it affects this application's use of the package, and what the remediation options are.

Triage a ruff S violation

Enable ruff's S rule set locally by adding it to pyproject.toml and run uv run ruff check --select S. For each violation reported, decide whether it represents a real risk in this application or a false positive. For violations you suppress, write an inline comment that explains why the suppression is safe. For violations that represent real risks, fix them.