GitHub Actions CI/CD (3/10): Quality Gate Before Tests — Lint and Formatting
Summary: Add
rufftohellocias 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.
| Key | Value |
|---|---|
| Package name | helloci |
| Python version | 3.12 |
| Working directory | ~/projects/helloci |
| Linter/formatter | ruff |
| Config location | pyproject.toml |
0. Prerequisites
- The
hellocipackage 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 topy310because 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 issuesruff format . --checkverifies consistent formattingcheck.shruns lint, format, and tests in one commandpyproject.tomldeclaresruffas 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.