Cơn ác mộng CI/CD: Mất 5 phút chỉ để check vài lỗi format
Nếu đang làm việc trong một team Python với repo từ vài chục nghìn dòng code trở lên, chắc hẳn bạn đã quen với cảnh này. Mỗi lần ai đó mở Pull Request, GitHub Actions lại xoay tít 3 đến 5 phút. Pipeline nghẽn chỉ để chờ một loạt tool chạy tuần tự:
- isort sắp xếp lại vị trí các dòng import.
- Black format lại thụt đầu dòng, dấu nháy và ngắt dòng theo PEP 8.
- Flake8 cùng mớ plugin (flake8-bugbear, flake8-docstrings…) quét lỗi tiềm ẩn.
- pyupgrade rà soát để nâng cấp cú pháp Python cũ.
Kết quả? Chi phí runner CI tăng chóng mặt. Thời gian chờ review code kéo dài vô ích.
Tệ nhất là ở máy local. Vì pre-commit hook chạy mất cả chục giây, anh em dev thường tiện tay gõ luôn git commit --no-verify cho nhanh. Thế là code đẩy lên CI lại tạch vì lỗi thừa khoảng trắng, làm vỡ nhịp làm việc của cả sprint.
Vì sao bộ ba Flake8, Black và isort vừa chậm vừa hay cãi nhau?
Có ba điểm nghẽn cốt lõi khiến combo truyền thống này ngày càng đuối sức khi dự án phình to:
- Gánh nặng từ Python runtime: Cả ba công cụ đều viết bằng Python. Mỗi lần kích hoạt, hệ điều hành phải khởi động trình thông dịch và nạp hàng tá thư viện vào RAM. Khâu khởi động này tạo ra độ trễ đáng kể.
- Parse cú pháp lặp đi lặp lại: Giả sử bạn có file
services/order.pydài 2.500 dòng. isort đọc file và dựng cây cú pháp (AST) một lần. Black vào việc, lại parse AST lần hai. Flake8 nhảy vào, parse lần thứ ba. Ba công cụ hoạt động độc lập, không chia sẻ bộ nhớ, gây lãng phí CPU khủng khiếp. - Quy tắc đè nhau chan chát: Black mặc định ngắt dòng ở 88 ký tự, nhưng Flake8 khăng khăng bắt lỗi
E501nếu vượt quá 79 ký tự. Khi Black chèn khoảng trắng trước dấu hai chấm ở slice indexing (nhưx[1: 2]), Flake8 lập tức báo lỗiE203. Cuối cùng, bạn phải đẻ thêm file.flake8,setup.cfgvàpyproject.tomlchỉ để dẹp mấy vụ xung đột ngớ ngẩn này.
Những cách “chữa cháy” từng thử nghiệm (và vì sao thất bại)
Trước khi tìm ra Ruff, team mình đã thử nhiều cách tối ưu tạm thời:
- Tách job chạy song song trên CI: Cho isort, Black và Flake8 chạy đồng thời trên 3 runner khác nhau. Thời gian chờ có giảm từ 4 phút xuống gần 2 phút, nhưng hóa đơn CI tăng gấp đôi vì ngốn quá nhiều slot máy ảo. Còn ở máy dev local, tốc độ pre-commit hook vẫn rùa bò như cũ.
- Gọt bớt plugin Flake8: Tắt bớt các rule nặng, chỉ giữ lại kiểm tra syntax tối thiểu. Tốc độ có cải thiện đôi chút, nhưng cái giá phải trả quá đắt: biến thừa, import rác và lỗi logic sót lại lọt thẳng vào branch
mainmà reviewer không để ý.
Bài học rút ra rất rõ: thay vì cố vá víu nhiều công cụ rời rạc, giải pháp dứt điểm là đưa toàn bộ khâu phân tích cú pháp về một engine duy nhất ở tầng hệ thống.
Giải pháp dứt điểm: Chuyển toàn bộ sang Ruff
Ruff là dự án mã nguồn mở do Charlie Marsh (sáng lập Astral) phát triển, viết hoàn toàn bằng Rust. Ruff không đơn thuần là linter. Đây là bộ công cụ tất-cả-trong-một, thay thế trọn vẹn Flake8, Black, isort, pyupgrade, autoflake cùng hơn 40 plugin phổ biến khác.
Ruff nhanh hơn các công cụ thuần Python từ 10 đến 100 lần. Dự án đạt được tốc độ này nhờ cơ chế xử lý đa luồng với Rayon, quản lý bộ nhớ trực tiếp trên AST và biên dịch thẳng ra mã máy.
1. Cài đặt Ruff vào dự án
Bạn có thể cài đặt Ruff cực kỳ nhanh qua pip, poetry hoặc uv:
# Cài qua pip
pip install ruff
# Hoặc thêm vào dev dependencies với Poetry
poetry add --group dev ruff
2. Gom gọn cấu hình vào pyproject.toml
Xóa sạch các file lẻ tẻ như .flake8 hay .isort.cfg. Giờ đây toàn bộ quy tắc nằm gọn trong file pyproject.toml:
[tool.ruff]
# Độ dài tối đa của dòng (khớp chuẩn Black)
line-length = 88
target-version = "py311"
# Bỏ qua các thư mục không cần quét
exclude = [
".git",
".venv",
"__pycache__",
"build",
"dist",
]
[tool.ruff.lint]
# Các bộ rule cần bật:
# E, W: Lỗi và cảnh báo style (thay pycodestyle/Flake8)
# F: Biến thừa, import sai (thay Pyflakes)
# I: Sắp xếp import (thay isort)
# B: Lỗi logic tiềm ẩn (thay flake8-bugbear)
# UP: Nâng cấp cú pháp mới (thay pyupgrade)
select = ["E", "W", "F", "I", "B", "UP"]
# Bỏ qua rule gây phiền hoặc đã có formatter xử lý
ignore = ["E501", "B008"]
[tool.ruff.lint.isort]
# Gom nhóm import nội bộ dự án
known-first-party = ["my_project_module"]
combine-as-imports = true
[tool.ruff.format]
# Định dạng code tương thích hoàn toàn với Black
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
3. Chạy kiểm tra và tự động sửa lỗi
Ruff phân tách nhiệm vụ rất rõ qua hai lệnh CLI chính:
ruff check: Đóng vai trò Linter (bắt biến không dùng, import rác, lỗi cú pháp).ruff format: Đóng vai trò Formatter (thay thế 100% Black).
Để quét và tự động sửa các lỗi an toàn:
# Quét và tự động sửa các lỗi safe-fix
ruff check . --fix
# Format lại toàn bộ codebase chỉ trong chớp mắt
ruff format .
4. Tích hợp Pre-commit Hook siêu nhẹ
Cập nhật file .pre-commit-config.yaml để bắt lỗi ngay khi dev gõ lệnh commit:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.8
hooks:
# Chạy Linter và tự sửa lỗi
- id: ruff
args: [--fix]
# Format code
- id: ruff-format
Thời gian chạy pre-commit lúc này chỉ mất khoảng 0.1 đến 0.2 giây. Nhanh đến mức dev còn chưa kịp nhận ra hook vừa chạy, chấm dứt hoàn toàn thói quen dùng --no-verify.
5. Tối ưu Pipeline CI/CD trên GitHub Actions
Trên CI, bạn có thể dọn sạch mớ cấu hình rườm rà trước đây và thay bằng một workflow ngắn gọn:
name: Code Quality Check
on: [push, pull_request]
jobs:
lint-and-format:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ruff
run: pip install ruff
- name: Run Ruff Linter
run: ruff check --output-format=github .
- name: Run Ruff Formatter
run: ruff format --check .
Thực tế kiểm chứng: với repo backend gồm 65.000 dòng code của team mình, thời gian chạy bước lint và format trên GitHub Actions giảm ngoạn mục từ 2 phút 20 giây xuống còn 1.4 giây. Không còn xung đột giữa linter và formatter. Anh em trong team cũng không còn phải tranh luận về dấu nháy đơn hay nháy kép trong mỗi buổi review code.

