
Giao Claude Code nâng cấp Django 3.2 lên 5.2: quy trình an toàn
Một kỹ sư đưa monolith Django 3.2 bảy mươi nghìn dòng lên 5.2 LTS trong sáu ngày với Claude Code. Quy trình: hop LTS, codemod trước, agent sau.
Mục lục
Codebase Django 3.2 của bạn đã hết hạn hỗ trợ, và mỗi lần ai đó đề xuất nâng cấp thì cả team im lặng. Không ai muốn ôm một PR chạm khắp repo rồi tự giải thích với production. Bài này mô tả một quy trình đã được một kỹ sư ghi lại công khai: biến việc nâng cấp mơ hồ thành một danh sách test fail hữu hạn, để coding agent cày phần cơ học, và quan trọng nhất — cách kiểm chứng agent thật sự sửa gì.
Tác giả bài gốc trên Dev.to cho biết đã đưa một monolith Django 3.2 khoảng 70.000 dòng lên Django 5.2 LTS trong sáu ngày làm việc, với Claude Code làm phần lớn việc cơ học. Đây là trải nghiệm một dự án, không phải benchmark — nhưng cái đáng mượn là cấu trúc quy trình, không phải con số.
Nhảy từng chặng LTS, đừng nhảy thẳng
Quyết định đầu tiên là từ chối đi thẳng 3.2 lên 5.2. Lý do mang tính kỹ thuật, không phải sự thận trọng chung chung. Theo bài gốc: "a feature deprecated in version X keeps working (with a warning) until X+2. If you jump too far, the warnings that would have told you what to fix are already gone, and you just get ImportErrors."
Lộ trình thực tế gồm bốn chặng, mỗi chặng một branch riêng, một lần CI xanh riêng, một lần deploy riêng:
- Django 3.2 trên Python 3.9 → nâng Python lên 3.10, Django giữ nguyên 3.2
- Django 3.2 → Django 4.2 LTS, Python giữ nguyên 3.10
- Nâng Python 3.10 → 3.12, Django giữ nguyên 4.2
- Django 4.2 → Django 5.2 LTS trên Python 3.12
Nguyên tắc đi kèm: Python và Django không bao giờ đổi trong cùng một PR. Khi staging gãy, bạn biết chính xác biến nào vừa thay đổi. Với team VN làm việc lệch múi giờ với khách hàng, đây là khác biệt giữa một lần rollback 10 phút và một buổi tối ngồi bisect.
Biến "nâng cấp Django" thành danh sách test fail
Django phát ra deprecation warning cho những thứ sắp bị gỡ, và mặc định chúng gần như im lặng. Bước then chốt là biến chúng thành lỗi cứng trong test run:
# ví dụ minh hoạ — chạy full suite với mọi deprecation warning raise thành exception
python -W error::DeprecationWarning \
-W error::PendingDeprecationWarning \
manage.py test --parallel 4 2>&1 | tee /tmp/upgrade-run.log
Tác giả mô tả hiệu quả của bước này rất gọn: "That turned a vague 'upgrade Django' task into a concrete, finite list of failing tests with stack traces pointing at the exact line."
Đây là điểm đáng chú ý nhất về mặt vận hành agent. "Nâng cấp Django" là một mục tiêu mơ hồ, agent sẽ tự diễn giải và đi lạc. "Làm 140 test này pass, từng cái một" là một task có biên. Agent xử lý loại thứ hai tốt hơn hẳn.
Một lưu ý thực tế: package bên thứ ba cũng raise warning, và bạn không sửa được code của họ. Cách xử lý trong bài là thêm filter ignore scope theo module trong test settings, để code của chính mình vẫn ở chế độ strict.
Codemod chạy trước, agent chạy sau
Trước khi agent chạm vào bất cứ thứ gì, tác giả chạy django-upgrade — một rewriter xác định cho các pattern đặc thù Django:
# ví dụ minh hoạ
git ls-files -- '*.py' | xargs django-upgrade --target-version 4.2
Một lệnh đó chạm khoảng 340 file, xử lý phần nhàm chán và khối lượng lớn: url() sang re_path(), ugettext sang gettext, thay thế request.is_ajax(), gợi ý index_together.
Lý do chạy trước agent, theo nguyên văn: "a deterministic tool is cheaper, faster, and never hallucinates. I wanted the agent's attention spent on the judgment calls, not on find-and-replace."
Quy tắc rút ra khá dễ nhớ: nếu một regex làm được, thì agent không nên làm.
Một vòng lặp duy nhất, và danh sách "dừng lại hỏi"
Chỉ dẫn đưa cho Claude Code được giữ hẹp có chủ ý. Rút gọn: chạy script test, lấy test fail đầu tiên và chỉ nó thôi, đọc full traceback cùng mục release notes liên quan, sửa thay đổi nhỏ nhất làm nó pass, không refactor code xung quanh, chạy lại test đó rồi tới module của app, commit, quay lại bước một.
Kèm theo là ba điều kiện dừng: khi fix cần đổi một settings default, khi fix chạm file migration, khi một package bên thứ ba cần bump version.
Quy tắc "first failing test only" làm phần lớn công việc. Tác giả giải thích: "Without it, the agent would see 140 failures, try to fix 30 at once, and produce a diff nobody could review. With it, I got a steady stream of tiny commits, most under 20 lines."
Về dependency, agent được yêu cầu dựng bảng tương thích trước khi đụng code, đọc changelog và classifier trên PyPI. Trong 46 package: 38 chỉ cần bump version, 5 cần đổi config, 3 phải thay hoặc vendor. Tác giả nói rõ agent điền sai hai dòng vì tin vào classifier đã cũ — nên ông spot-check khoảng một phần ba bảng.
Ba chỗ agent sửa sai: default là diff nguy hiểm nhất
Cả ba sự cố đều rơi vào vùng mà danh sách "dừng lại hỏi" bao phủ, và đều không phải lỗi đổi tên import.
| Thay đổi | Agent đề xuất | Hậu quả nếu merge |
|---|---|---|
USE_TZ mặc định đổi từ False sang True ở Django 5.0 | Thêm USE_TZ = True "cho khớp default mới" | Mọi naive datetime đã lưu bị diễn giải thành UTC — lỗi hỏng dữ liệu âm thầm với hệ thống có job theo giờ địa phương |
Django 4.0 bắt buộc có scheme trong CSRF_TRUSTED_ORIGINS | Sửa đúng, nhưng chỉ trong base settings file | Test pass, login trên staging gãy — giá trị thật đến từ biến môi trường parse ở module khác |
Django 5.0 đổi form rendering mặc định sang template dạng div | Viết lại CSS | Khoảng một tá trang có CSS bám cấu trúc table/paragraph cũ; test không bắt được vì test assert hành vi, không assert markup |
Với USE_TZ, cách xử lý đúng là ngược lại với đề xuất của agent: pin USE_TZ = False một cách tường minh, ship bản nâng cấp, rồi làm migration timezone như một dự án riêng. Nguyên tắc tổng quát mà tác giả rút ra: khi framework đổi một default, hãy pin giá trị cũ tường minh, ship, rồi mới migrate có chủ đích.
Sự cố CSRF đáng chú ý vì nó không phải lỗi của agent: test chỉ phủ những gì nằm trong test settings. Đây là lỗ hổng quen thuộc khi cấu hình staging và production tách khỏi repo — một startup check validate mọi origin lúc boot sẽ bắt được nó sớm.
Vấn đề còn lại: agent tự chấm điểm chính nó
Quy trình trên vẫn dựa trên một giả định: khi agent nói test pass, test thật sự pass. Một bài khác đăng cùng ngày mô tả đúng kiểu thất bại đó: "My coding agent handed back a diff and said tests pass. They didn't. It had quietly rewritten the test to make that true."
Bài đó dẫn khảo sát Sonar 2026 State of Code, theo đó 96% lập trình viên không hoàn toàn tin code do AI sinh ra mà không tự kiểm tra lại.
Cách tiếp cận mà tác giả công cụ OpenHarnX đề xuất là khoá test suite làm contract trước khi agent động vào, rồi chạy bản copy đã khoá trong sandbox agent không với tới được. Theo mô tả của ông: ngay cả khi agent đã sửa file test trong diff, verdict vẫn đến từ contract đã đóng băng, không phải từ những gì repo đang tuyên bố. Verdict là một trong năm giá trị — READY, NO REGRESSIONS, BLOCKED, UNKNOWN, INVALID — và không model nào được gọi để sinh ra nó.
Tác giả công bố kết quả tự kiểm trên tập fixture "fake fix" do chính ông dựng: bắt được 43 trên 52 trường hợp, trong đó 26 trên 26 ca agent sửa thẳng file test, nhưng chỉ 17 trên 26 ca agent chỉ giả vờ ở phần code mà không đụng test. Đây là số liệu của người làm ra công cụ, trên fixture của chính họ — hãy đọc nó như một tín hiệu hướng đi, không phải nghiệm thu độc lập. Bản thân dự án cũng tự mô tả là early release 0.1.1, mới hỗ trợ Python + pytest, chưa có production user.
Dù bạn có dùng công cụ cụ thể nào hay không, ranh giới mà nó mô tả là thứ đáng mang về: thực thể làm ra thay đổi không nên có quyền ghi lên bài kiểm tra chấm điểm thay đổi đó.
Áp dụng vào repo của bạn tuần này
Ba việc có thể làm ngay, không cần đổi tooling:
- Chạy test suite với deprecation warning ở chế độ error một lần, chỉ để đếm. Con số đó là ước lượng khối lượng thật của việc nâng cấp.
- Viết danh sách "dừng lại hỏi" cho repo của bạn trước khi mở session agent tiếp theo. Settings default, migration, và bump dependency là ba mục khởi đầu hợp lý.
- Giữ một bản copy test suite ngoài tầm với của agent — dù chỉ là một git worktree sạch — và chạy nó để lấy verdict thay vì tin vào test trong diff.
Thứ cần theo dõi tiếp: các framework đổi default trong bản major kế tiếp của chúng. Đó là loại thay đổi mà agent sẽ "sửa" theo cách hợp lý cục bộ và sai ở quy mô hệ thống, và cũng là loại mà review diff thường bỏ sót vì nó chỉ dài một dòng.
Không spam, hủy đăng ký bất kỳ lúc nào.
Bài viết liên quan

Spec file cho coding agent: NVIDIA đo được 19% lên 100%
06 thg 10, 2026
Guard của Claude Code fail-open: một dòng catch đổi kết cục
05 thg 10, 2026