Skip to content

Running Linters as CI Gate


Your workflow can install dependencies, but nothing in it actually inspects your code yet. A linter is the cheapest possible check to add — it catches an entire category of mistakes before a single test even runs.

What A Linter Actually Catches

A linter doesn’t know if your code is correct — it can’t tell you send_file.py sends the wrong bytes. What it catches is a narrower, mechanical set of problems: unused imports, undefined names, obvious style violations, syntax that technically runs but will confuse the next person who reads it. Cheap to check, cheap to fix, and exactly the kind of thing that’s easy to miss in a manual review but trivial for a tool to flag in under a second.

Running it as a CI gate — a step that fails the workflow if it finds a problem — means that class of mistake can’t quietly land on main.

Choosing Ruff

There are several Python linters (flake8 being one long-standing option), but this course uses Ruff. It’s written in Rust, runs dramatically faster than older pure-Python linters, and — relevant here — it ships an official GitHub Action that makes wiring it into a workflow a one-step affair.

Adding ruff-action To The Workflow

- name: Lint (ruff)
  uses: astral-sh/[email protected]

By default, this does two things: it installs Ruff on the runner, then runs ruff check against your repository. ruff check is the linting pass — it looks for the kinds of issues described above and exits with a non-zero status if it finds any, which is what makes the workflow fail.

You don’t need a config file for Ruff to work — it ships with a sensible default rule set out of the box. If the project doesn’t have a pyproject.toml or ruff.toml, Ruff just runs with its defaults.

Note

ruff-action adds ruff to the runner’s PATH as a side effect, so if you wanted to run additional Ruff commands (like ruff format --check) in a later step, you could — without installing it again.

Reading Ruff’s Output When It Fails

When ruff check finds something, the log shows the file, line, column, a rule code, and a short description — for example:

send_file.py:3:8: F401 [*] `socket` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.

The F401 is Ruff’s rule code for the issue category (in this case, an unused import). Ruff’s rule codes are searchable in its documentation if a message alone isn’t enough context — but for most everyday issues, the description on that line tells you exactly what to fix.

Hands-On: Trigger A Real Failure, Then Fix It

Rather than trust that this works, prove it — break the lint check on purpose, watch it catch the problem, then fix it for real.

Add a genuinely unused import to send_file.py. Open the file and add this line near the top, without using it anywhere:

sed -i '1i import json' send_file.py

Update .github/workflows/ci.yml to add the lint step after dependency installation:

name: CI

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

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v7

      - name: Setup Python
        uses: actions/setup-python@v7
        with:
          python-version: "3.13"
          cache: "pip"
          cache-dependency-path: requirements.txt

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install -r requirements.txt

      - name: Lint (ruff)
        uses: astral-sh/[email protected]

Commit and push both changes:

git add send_file.py .github/workflows/ci.yml
git commit -m "Add ruff lint step (and a deliberate unused import to test it)"
git push

Open the Actions tab. The “Lint (ruff)” step should fail, and the log should point straight at the import json line with an F401 unused-import warning. That’s the gate doing its job — a broken run never gets a false green checkmark.

Error: send_file.py:1:1: I001 Import block is un-sorted or un-formatted
  help: Organize imports
Error: send_file.py:1:8: F401 `json` imported but unused
  help: Remove unused import: `json`
Error: The process '/opt/hostedtoolcache/ruff/0.16.7/x86_64/ruff' failed with exit code 1

Now fix it for real:

sed -i '/^import json$/d' send_file.py
git add send_file.py
git commit -m "Remove unused import"
git push

Push again and confirm the lint step goes green. You’ve now got a working CI gate that catches this exact class of mistake automatically, on every push, without anyone having to remember to run a linter by hand.

The workflow still isn’t running your actual test suite, though — a clean lint pass proves your code is tidy, not that it works. That’s what the next chapter adds.

Last updated on