Jenkins CI/CD (5/11): Fast-Fail Static Checks With a Lint Stage
Summary: You add a
rufflint stage to the Jenkinsfile that runs before unit tests, so cheap static checks catch unused imports, syntax issues, and style violations in seconds — failing the build fast before expensive test suites even start.
Example Values Used in This Tutorial
| Key | Value |
|---|---|
| Linter | ruff |
| Lint command | .venv/bin/ruff check src/ tests/ |
| Config location | pyproject.toml [tool.ruff] section |
| Stage position | Before unit tests |
| Previous parts | Parts 1-4 completed |
0. Prerequisites
- A working Jenkins controller at
http://localhost:8080(Part 1). - The
hellociPython package with passing unit tests committed to Git (Part 2). - A Jenkins pipeline job that runs pytest and publishes JUnit XML results (Parts 3-4).
- Your current Jenkinsfile has stages for Setup Python, Install Dependencies, and Run Unit Tests with a
post { always { junit } }block.
Note: Your pipeline from Part 4 should be green. If it is not, go back and fix it before continuing. This tutorial adds a new stage to that working pipeline.
1. Why Lint Before Tests
Not all checks are equally expensive. Running a full test suite can take minutes. A linter scans your source files in under a second. If you have an unused import or a syntax error, there is no reason to wait for 200 tests to run before finding out.
The principle is simple: cheapest checks first, most expensive last. A lint stage at the front of your pipeline acts as a fast-fail gate. If the code has obvious problems, the build stops immediately and the developer gets feedback in seconds instead of minutes.
Here is how the cost stacks up:
| Check | Typical time | What it catches |
|---|---|---|
| Linting | 1-2 seconds | Unused imports, undefined names, syntax errors, style violations |
| Unit tests | 10-60 seconds | Logic bugs, regressions, incorrect return values |
| Integration tests | 1-5 minutes | Database issues, service communication failures, environment problems |
By ordering your pipeline stages from cheapest to most expensive, you keep the feedback loop as short as possible. A developer who pushed a file with an unused import finds out in 5 seconds, not after waiting 3 minutes for the full suite to run.
2. Install Ruff Locally
Ruff is a fast Python linter written in Rust. It checks for hundreds of common issues and runs orders of magnitude faster than traditional Python linters like flake8 or pylint.
Install ruff into your virtual environment:
cd ~/projects/helloci
source .venv/bin/activate
pip install ruffCode language: Shell Session (shell)
Verify it works:
ruff versionCode language: Shell Session (shell)
ruff 0.8.6Code language: Shell Session (shell)
Tip: Your version number will differ. Any recent version of ruff works for this tutorial.
3. Add Ruff to Test Dependencies
Open pyproject.toml and add ruff to your [project.optional-dependencies] test group. After editing, the section should look like this:
[project.optional-dependencies]
test = [
"pytest",
"ruff",
]Code language: TOML, also INI (ini)
This ensures that when your Jenkinsfile runs .venv/bin/pip install -e ".[test]", ruff is installed automatically alongside pytest. Every dependency your pipeline needs must be declared here — no manual pip install commands scattered through your Jenkinsfile.
Reinstall locally to confirm the dependency resolves:
pip install -e ".[test]"Code language: Shell Session (shell)
4. Run Ruff Locally
Run ruff against your source and test directories:
ruff check src/ tests/Code language: Shell Session (shell)
If your code is clean, you see:
All checks passed!Code language: Shell Session (shell)
If ruff finds issues, it reports them with file, line number, rule code, and a description:
src/helloci/__init__.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.Code language: Shell Session (shell)
Ruff uses rule codes like F401 (unused import) and E501 (line too long). The [*] marker means ruff can auto-fix the issue with --fix. For CI you want the check-only mode — let the build fail and make the developer fix the code intentionally.
Note: If ruff reports errors in your existing code, fix them now before continuing. Your pipeline should start from a clean baseline.
5. Configure Ruff
Ruff works out of the box with sensible defaults, but you should add a minimal configuration to make your choices explicit. Add a [tool.ruff] section to pyproject.toml:
[tool.ruff]
target-version = "py312"
line-length = 88
[tool.ruff.lint]
select = ["E", "F", "I"]Code language: TOML, also INI (ini)
Here is what each setting does:
| Setting | Value | Purpose |
|---|---|---|
target-version | "py312" | Tell ruff which Python version your code targets |
line-length | 88 | Maximum line length (matches Black’s default) |
select | ["E", "F", "I"] | Enable pycodestyle errors, Pyflakes checks, and isort import sorting |
The select list controls which rule families ruff enforces. Starting with E, F, and I gives you solid coverage without being overwhelming:
- E — pycodestyle errors (whitespace issues, indentation problems).
- F — Pyflakes (unused imports, undefined names, redefined variables).
- I — isort (import ordering and grouping).
You can expand this list later as your project matures. For now, these three families catch the most common issues.
Run ruff again to confirm the configuration is picked up:
ruff check src/ tests/Code language: Shell Session (shell)
6. Update the Jenkinsfile
Open the Jenkinsfile in your repository root. Add a Lint stage between Install Dependencies and Run Unit Tests. Here is the complete updated Jenkinsfile:
pipeline {
agent any
stages {
stage('Setup Python') {
steps {
sh 'python3 -m venv .venv'
sh '.venv/bin/pip install --upgrade pip'
}
}
stage('Install Dependencies') {
steps {
sh '.venv/bin/pip install -e ".[test]"'
}
}
stage('Lint') {
steps {
sh '.venv/bin/ruff check src/ tests/'
}
}
stage('Run Unit Tests') {
steps {
sh 'mkdir -p results'
sh '.venv/bin/pytest tests/ --junitxml=results/junit.xml'
}
}
}
post {
always {
junit 'results/junit.xml'
}
}
}Code language: Groovy (groovy)
The only change from Part 4 is the new Lint stage. Everything else — the venv setup, dependency installation, pytest with JUnit XML, and the post block — stays exactly the same.
The position of the Lint stage matters. It runs after dependencies are installed (so ruff is available) and before the test stage (so lint failures stop the build before tests run). If ruff exits with a non-zero code, Jenkins marks the Lint stage as failed and skips all subsequent stages. The post { always } block still runs, so Jenkins still tries to collect the JUnit XML — but since the test stage never ran, the file will not exist. Jenkins handles this gracefully with a warning.
7. Trigger the Build — Lint Passes
Commit and push all the changes:
git add pyproject.toml Jenkinsfile
git commit -m "Add ruff lint stage to pipeline"
git push origin mainCode language: Shell Session (shell)
Go to your pipeline job in Jenkins and click Build Now.
Watch the Stage View as the build progresses. You now see four stages instead of three:
- Setup Python — creates the venv.
- Install Dependencies — installs helloci and ruff.
- Lint — runs ruff check (completes in a few seconds).
- Run Unit Tests — runs pytest with JUnit output.
All four stages should be green. The Lint stage proves that your code is clean and ruff is wired into the pipeline correctly.
Tip: Click the Lint stage box in the Stage View to see the console output. You should see ruff’s “All checks passed!” confirmation.
8. Trigger a Lint Failure
The real value of a lint stage shows when something breaks. Intentionally introduce a lint error to see the fast-fail behavior.
Open src/helloci/__init__.py and add an unused import at the top of the file:
import osCode language: Python (python)
Do not use os anywhere in the file. This is a classic F401 violation — an import that does nothing and clutters the namespace.
Commit and push:
git add src/helloci/__init__.py
git commit -m "Add unused import to trigger lint failure"
git push origin mainCode language: Shell Session (shell)
Trigger the build in Jenkins. Watch the Stage View. This time:
- Setup Python — green.
- Install Dependencies — green.
- Lint — red.
- Run Unit Tests — skipped.
The Lint stage fails because ruff detects the unused import and exits with a non-zero code. Jenkins stops the pipeline immediately. The test stage never runs. You saved all the time that the test suite would have taken.
Click the failed Lint stage to see the console output:
src/helloci/__init__.py:1:8: F401 [*] `os` imported but unused
Found 1 error.
[*] 1 fixable with the `--fix` option.Code language: Shell Session (shell)
The failure message tells the developer exactly what is wrong, in which file, on which line, and what the rule code is. No guesswork required.
9. Fix the Error and Restore Green
Remove the import os line from src/helloci/__init__.py.
Run ruff locally to confirm the fix:
ruff check src/ tests/Code language: Shell Session (shell)
All checks passed!Code language: Shell Session (shell)
Commit and push:
git add src/helloci/__init__.py
git commit -m "Remove unused import — fix lint failure"
git push origin mainCode language: Shell Session (shell)
Trigger the build in Jenkins. All four stages go green. The Stage View now shows the recovery pattern: a red Lint stage on the previous build, a green Lint stage on this one. This is the fast-fail feedback loop in action — the developer broke something, got immediate feedback, fixed it, and confirmed the fix in a matter of minutes.
10. The Pipeline Ordering Principle
Your pipeline now has four stages, ordered by cost:
| Stage | Cost | What fails here |
|---|---|---|
| Setup Python | ~5 seconds | Missing Python, broken venv |
| Install Dependencies | ~10 seconds | Dependency resolution errors, typos in package names |
| Lint | ~2 seconds | Unused imports, style violations, syntax issues |
| Run Unit Tests | ~15-60 seconds | Logic bugs, regressions |
This ordering is not an accident. It follows a principle that applies to every CI/CD pipeline, regardless of language or tooling: run the cheapest checks first and the most expensive checks last.
As your pipeline grows in later parts of this series, you will add integration tests (minutes), artifact builds, and deployment steps. Each new stage slots into the pipeline at a position that matches its cost. Fast checks gate slow checks. If the code cannot pass a two-second lint, there is no reason to spin up a Postgres container for integration tests.
Warning: Resist the temptation to combine lint and test into a single stage. Separate stages give you separate pass/fail indicators in the Stage View, separate timing data, and separate console logs. When something fails, you want to know instantly which category of check caught the problem.
Summary
You added a ruff lint stage that runs before unit tests, giving you fast-fail static analysis in your Jenkins pipeline. Here is what you accomplished:
- Installed
ruffand added it to the[project.optional-dependencies]test group inpyproject.toml. - Configured ruff with a
[tool.ruff]section targeting Python 3.12 withE,F, andIrule families. - Added a
Lintstage to the Jenkinsfile between Install Dependencies and Run Unit Tests. - Triggered a green build to confirm ruff is wired in correctly.
- Intentionally introduced an unused import to see the fast-fail behavior — the Lint stage went red and the test stage was skipped entirely.
- Fixed the error and confirmed the pipeline recovered to all-green.
- Established the pipeline ordering principle: cheapest checks first, most expensive last.
Your pipeline now catches simple mistakes in seconds instead of minutes. Next up in Part 6: you add integration tests with Docker Compose and a real Postgres database, slotting them in after unit tests as the most expensive check in the pipeline.