Case study CI/CD fail: một dòng Tera parse lỗi kéo 5 PR theo
Vị trí quảng cáo đang chờ kích hoạt
TL;DR: Một cú pháp Tera sai trong macro ảnh của blog này từng chặn toàn bộ deploy production một buổi sáng. Điều thú vị không phải là lỗi gốc — mà là cách nó kéo theo hàng loạt sự cố phụ:
self::gọi sai namespace,filter(...) ~ concatkhông hợp lệ, rồi auto-merge lại chốt nhầm một bản fix CŨ, khiếnmaintưởng đã sửa nhưng thật ra vẫn hỏng. Bài này kể lại nguyên vẹn chuỗi sự kiện đó, kể cả những chỗ mọi thứ đi sai hướng trước khi tìm ra đúng nguyên nhân.
Ở phần 1 của series này, mình đã nói về mô hình vaccine CI/CD ở tầng lý thuyết. Đây là case study CI/CD fail Tera parse — một ca thật, xảy ra trong một buổi làm việc bình thường — bắt đầu chỉ từ một macro Tera dùng để chọn ảnh đại diện cho bài viết.
Case study CI/CD fail Tera parse bắt đầu từ đâu: một dòng cú pháp không hợp lệ
Zola (static site generator viết bằng Rust) dùng Tera làm template engine. Macro img::cover_src chịu trách nhiệm chọn ảnh cover theo thứ tự ưu tiên: ảnh khai báo thủ công → ảnh auto-path theo slug → ảnh editorial tự sinh → placeholder. Để tính đường dẫn auto theo slug, code gốc viết:
{{- page.components | join(sep="/") ~ "/" ~ page.slug -}}
zola build báo lỗi ngay từ bước parse template, trước khi render bất kỳ trang nào:
ERROR Failed to parse "templates/macros/img.html"
= expected `or`, `and`, `not`, ... or a variable end (`}}`)
Nguyên nhân: Tera không chấp nhận nối một filter (| join(...)) trực tiếp với toán tử ~ trong cùng một expression. Tài liệu Tera chính thức mô tả ~ là toán tử nối chuỗi, nhưng không đảm bảo nó áp dụng an toàn ngay sau một filter call có tham số — toàn bộ chỗ dùng ~ đang chạy đúng trong codebase đều chỉ nối biến hoặc chuỗi thuần, chưa bao giờ nối thẳng kết quả một filter. Cách sửa đúng là tách filter ra một biến riêng trước:
Vị trí quảng cáo đang chờ kích hoạt
{%- set components_path = page.components | join(sep="/") -%}
{%- set feat_key = components_path ~ "/" ~ page.slug -%}
Nghe thì đơn giản — và đúng là bản thân lỗi này không khó. Cái khó nằm ở những gì xảy ra tiếp theo.
Sửa lần 1 vẫn sai: đổi cách gọi, dính lỗi khác
Lần sửa đầu tiên đổi hướng khác: tách logic tính key thành một macro riêng (slug_feature_key), rồi gọi nó bằng self::slug_feature_key(page=page). Build tiến xa hơn — parse qua được — nhưng vỡ ở bước RENDER với lỗi hoàn toàn khác:
Reason: Macro `self::slug_feature_key` not found in template `base.html`
Đây là một đặc điểm dễ hiểu nhầm của Tera: self:: bên trong một macro không trỏ về file định nghĩa macro đó, mà trỏ về template đang render TẠI GỐC (entry template). Vì cover_src được gọi từ base.html (thông qua import), self:: lúc runtime lại tìm slug_feature_key trong base.html — nơi nó không tồn tại. Bài học: self:: chỉ an toàn khi macro gọi và macro được gọi nằm trong đúng cùng một file, không đi qua chuỗi import.
Sửa lần 2: quay lại đúng gốc, nhưng auto-merge chốt nhầm bản cũ
Sau khi hiểu rõ nguyên nhân, cách sửa đúng quay lại chính là tách biến trước khi nối ~ như ở trên. Push lên, CI chạy lại, nhưng lúc kiểm tra thì phát hiện một chuyện lạ hơn cả hai lỗi trước: PR đã hiển thị "MERGED", với đúng tiêu đề PR, nhưng file trên main vẫn còn nguyên lỗi — không phải lỗi gốc, cũng không phải lỗi mới, mà là bản sửa lần 1 (còn lỗi self::).
Hoá ra: auto-merge trong pipeline này squash-merge ngay khi các check không phải qa-check (kt9-check, pr-template, rule-check) đã xanh — merge chốt lại đúng nhánh tại thời điểm đó, không tự "đợi" thêm commit fix tiếp theo được push sau. Bản fix cuối cùng (tách biến đúng) được push sau thời điểm merge, nên không bao giờ thật sự lên main qua PR đó.
Cách phát hiện ra vấn đề này không phải bằng cách tin vào tên workflow ("qa-check: success") mà bằng cách tra cứu theo đúng SHA commit:
gh api repos/<owner>/<repo>/commits/<sha>/check-runs
So sánh nội dung file thật trên main với commit local mới nhất mới lộ ra: hai bên khác nhau. Đây là bảng tổng kết trạng thái toàn bộ Pull Request liên quan tại đúng thời điểm đó, trước khi phát hiện ra merge đã chốt nhầm bản cũ:
| PR | Trạng thái | Vai trò |
|---|---|---|
| #1437 | MERGED | Fix ban đầu — nhưng auto-merge đã squash nhầm snapshot cũ (vẫn còn lỗi self::) |
| #1439 | MERGED | Hotfix thật — áp lại đúng bản tách biến trước khi nối ~ trên main |
| #1434 | MERGED | PR nội dung không liên quan, chỉ bị vạ lây vì cùng base main đang lỗi |
| #1436 | MERGED | PR nội dung khác, cũng bị chặn build tới khi #1439 lên |
| #1438 | MERGED | PR mang tới chính script QA sau này gây false positive (phần tiếp theo) |
Giải pháp không phải cố sửa tiếp PR đã đóng (không thể), mà là mở một PR mới, cắt thẳng từ main hiện tại, áp lại đúng bản fix, và merge lại — lần này verify bằng đúng SHA trước khi tin là xong.
Lỗi thứ ba: false positive từ một trang report vô tội
Tưởng vậy là xong, nhưng sau khi lỗi Tera đã fix thật, deploy vẫn đỏ — lần này vì một script QA khác (check_adsense_viewability.py) báo:
[P0] adsense-loaded-while-inactive: placeholder/off mode must not render AdSense code
Script này quét toàn bộ HTML đã build, tìm chuỗi "adsbygoogle" để xác nhận không có mã quảng cáo thật nào bị render khi hệ thống đang ở chế độ placeholder (chưa duyệt AdSense). Vấn đề: một trang report nội bộ nói VỀ tình trạng AdSense (content/ad-report.md) có đoạn văn xuôi kiểu "Mã adsbygoogle chưa cài, đang ở giai đoạn pre-application" — chỉ là MÔ TẢ bằng chữ, không phải mã quảng cáo thật. Nhưng script chỉ tìm substring "adsbygoogle" xuất hiện ở BẤT KỲ đâu trong HTML, nên coi luôn đoạn văn xuôi đó là bằng chứng ads đang render.
Cách sửa đúng: thu hẹp điều kiện phát hiện về đúng markup thật (thẻ <ins> mang class token adsbygoogle — chỉ xuất hiện khi macro render nhánh live), bỏ nhánh so khớp chuỗi trần quá rộng. Kèm theo đó là viết 2 test hồi quy: một test xác nhận trang chỉ NHẮC tới từ khoá không bị chặn nhầm, một test xác nhận markup THẬT vẫn bị phát hiện đúng — để lỗi này không lặp lại theo hướng ngược lại (bỏ sót ads thật).
Ba bài học rút ra từ một buổi sáng
- Một lỗi cú pháp có thể chặn toàn bộ deploy, vì template dùng chung được parse trước khi render bất kỳ trang nào — không có khái niệm "chỉ ảnh hưởng 1 trang" ở tầng parse.
- "PR đã merged" không đồng nghĩa "bản fix đúng đã lên main" nếu bạn còn tiếp tục push trong lúc CI của commit trước đang chạy. Luôn verify theo SHA cụ thể, không tin theo tên workflow.
- QA script tự viết cũng có thể sai theo hướng quá nhạy (false positive) — và false positive nguy hiểm không kém false negative, vì nó khiến người xử lý nghi ngờ sai chỗ, mất thời gian đi sửa nhầm hướng.
Phần 3 của series sẽ đi vào phần thực hành: cách một lệnh duy nhất (kt9 --fix) giúp quét toàn bộ trạng thái PR/CI/deploy, tự sửa những gì an toàn, và chỉ ra đúng chỗ cần con người can thiệp — áp dụng trực tiếp từ chính ca lỗi vừa kể ở trên.
Đọc thêm
- Vaccine CI/CD tự động hoá: học từ lỗi build, không lặp lại
- Merged is not live: vì sao PR merge xong chưa chắc đã lên production
- Đọc bảng Deploy History D/B/A/F: hướng dẫn chẩn đoán deploy status nhanh
- Backend tụt sau main: phát hiện và xử lý production drift với Snapshot Production
- Dùng
codex doctor,codex mcpvàconfig.tomlđể tự chẩn đoán Codex CLI - Chuyên mục Công nghệ +++
Liên kết bên ngoài được sử dụng trong bài viết
Liên kết nội bộ liên quan
- phần 1 của series này
- Merged is not live: vì sao PR merge xong chưa chắc đã lên production
- Đọc bảng Deploy History D/B/A/F: hướng dẫn chẩn đoán deploy status nhanh
- Backend tụt sau main: phát hiện và xử lý production drift với Snapshot Production
- Dùng `codex doctor`, `codex mcp` và `config.toml` để tự chẩn đoán Codex CLI
- Chuyên mục Công nghệ
Bản quyền & Ghi nguồn
Một phần dữ liệu trong bài viết được tham khảo từ Tài liệu Tera chính thức. Mọi thương hiệu, tên sản phẩm và tài liệu gốc thuộc quyền sở hữu của chủ sở hữu tương ứng. Bài viết chỉ trích dẫn, tổng hợp và phân tích — không nhằm thay thế tài liệu chính thức.
FAQ - Câu hỏi thường gặp
Vì sao một lỗi cú pháp nhỏ trong 1 file template lại chặn được toàn bộ deploy?
Auto-merge squash một bản fix CŨ nghĩa là gì?
Làm sao biết một PR đã merge có thực sự chứa đúng bản fix mới nhất không?
False positive trong QA script nguy hiểm ở điểm nào?
Vị trí quảng cáo đang chờ kích hoạt
Bình luận
Đang tải bình luận…
Chưa có bình luận nào. Hãy là người đầu tiên chia sẻ ý kiến.
Đăng nhập để tham gia thảo luận.
Đăng nhập bằng Google để bình luậnChỉ dùng để bình luận. Không truy cập trình soạn thảo/CMS.
Không kết nối được máy chủ. Vui lòng thử lại.