| |

GitHub Actions CI/CD (3/10): Quality Gate Before Tests — Lint and Formatting

Summary: Add ruff to helloci as a linter and formatter. Lint checks catch bugs that tests miss — unused imports, undefined variables, style inconsistencies — and they run in under a second. This becomes the cheapest gate in your CI pipeline.

KeyValue
Package namehelloci
Python version3.12
Working directory~/projects/helloci
Linter/formatterruff
Config locationpyproject.toml

0. Prerequisites

  • The helloci package from Part 1 with tests from Part 2
  • Virtual environment activated (source .venv/bin/activate)
  • All 11 unit tests passing (pytest -v)

1. Install Ruff

pip install ruffCode language: Shell Session (shell)
Successfully installed ruff-0.x.xCode language: Shell Session (shell)

Ruff is a Python linter and formatter written in Rust. It replaces flake8, isort, black, and several other tools with a single binary that runs in milliseconds.


2. Configure Ruff in pyproject.toml

Add the following sections to the bottom of your pyproject.toml.

[tool.ruff]
target-version = "py310"
src = ["src", "tests"]

[tool.ruff.lint]
select = [
    "E",    # pycodestyle errors
    "W",    # pycodestyle warnings
    "F",    # pyflakes (unused imports, undefined names)
    "I",    # isort (import ordering)
    "UP",   # pyupgrade (modernize syntax)
    "B",    # flake8-bugbear (common pitfalls)
    "SIM",  # flake8-simplify (simplifiable constructs)
]Code language: TOML, also INI (ini)

Key settings:

  • target-version — tells ruff which Python version to lint for. Set to py310 because your package supports >=3.10.
  • src — tells ruff where to find first-party imports for sorting.
  • select — chooses which rule sets to enable. Each letter code is a family of checks.

Tip: Start with a small set of rules and expand later. The set above catches the most common problems without overwhelming you with noise.


3. Run the Linter

ruff check .Code language: Shell Session (shell)

If your code is clean, you see no output and a zero exit code. If ruff finds issues, it lists them with file, line, and rule code.

src/helloci/cli.py:4:1: F401 [*] `sys` imported but unusedCode language: Shell Session (shell)

The format is file:line:column: RULE message. The [*] marker means ruff can fix this automatically.


4. Auto-Fix Lint Issues

Ruff can fix many issues automatically.

ruff check . --fixCode language: Shell Session (shell)

This rewrites files in place — removing unused imports, sorting import statements, and applying safe transformations. Always review the changes afterward.

git diffCode language: Shell Session (shell)

Warning: Auto-fix modifies your source files. Run your tests after any auto-fix to make sure nothing broke.


5. Run the Formatter

Ruff also formats code (like black).

ruff format .Code language: Shell Session (shell)
2 files reformattedCode language: Shell Session (shell)

To check formatting without changing files, use the --check flag.

ruff format . --checkCode language: Shell Session (shell)
2 files already formattedCode language: Shell Session (shell)

When --check finds unformatted files, it exits with a non-zero code — which is what makes it useful in CI. A non-zero exit code tells the CI runner “this step failed.”


6. Add Ruff to Your Project Dependencies

Add ruff to the test dependency group in pyproject.toml.

[project.optional-dependencies]
test = [
    "pytest>=8.0",
    "ruff>=0.4",
]Code language: TOML, also INI (ini)

Now anyone can install all development tools with one command.

pip install -e ".[test]"Code language: Shell Session (shell)

7. Create a Local Check Script

Create a file called check.sh in the project root that runs all checks in order.

#!/usr/bin/env bash
set -euo pipefail

echo "==> Lint"
ruff check .

echo "==> Format check"
ruff format . --check

echo "==> Tests"
pytest -vCode language: Shell Session (shell)

Make it executable.

chmod +x check.shCode language: Shell Session (shell)

Run it.

./check.shCode language: Shell Session (shell)
==> Lint
All checks passed!
==> Format check
2 files already formatted
==> Tests
...
11 passedCode language: Shell Session (shell)

The set -euo pipefail line makes the script stop on the first failure. This is exactly the behavior you want — if linting fails, there is no point running tests.

This script mirrors what your CI workflow will do in Part 4. Running it locally before pushing saves you from waiting for a remote CI run to tell you about a missing import.


8. Verify Your Project Structure

Your project should now look like this.

~/projects/helloci/
├── check.sh
├── pyproject.toml
├── src/
│   └── helloci/
│       ├── __init__.py
│       └── cli.py
├── tests/
│   ├── __init__.py
│   ├── test_cli.py
│   └── test_greet.py
└── .venv/Code language: Shell Session (shell)

Summary

You added ruff as a linter and formatter for helloci. Lint checks catch bugs that tests cannot — unused imports, undefined variables, inconsistent formatting — and they run in milliseconds.

  • ruff check . finds code quality issues
  • ruff format . --check verifies consistent formatting
  • check.sh runs lint, format, and tests in one command
  • pyproject.toml declares ruff as a development dependency

You now have two quality gates that run locally: lint/format (cheap, instant) and unit tests (fast, deterministic). In Part 4 you will move these exact checks into a GitHub Actions workflow so they run automatically on every push and pull request.

Similar Posts

Leave a Reply