Cải Thiện Hệ Thống QA & CI/CD Cho Blog Zola 503+ Trang

Duy Nguyen

Cải Thiện Hệ Thống QA & CI/CD Cho Blog Zola 503+ Trang

Khi blog kỹ thuật phát triển qua 500 trang, hệ thống CI/CD vốn ổn định bỗng đỏ liên tục. Bài này ghi lại hành trình chẩn đoán, sửa và cấu trúc lại pipeline QA — kèm 5 bài học rút ra cho ai vận hành site tĩnh lớn trên GitHub Actions.

Vấn đề: Khi blog lớn quá, CI/CD không chịu xong

Mọi chuyện bắt đầu khi tôi thêm tính năng mới. Push code lên, kiểm tra chất lượng (QA Gatekeeper) báo đỏ. Lần đầu tôi nghĩ lỗi ở mình — template sai syntax, Tera filter không hợp lệ. Sửa hết, push lại. Vẫn đỏ.

Nhìn kỹ log: workflow chạy được 5 giây rồi fail. Nguyên nhân: actions/setup-node@v6 với tham số cache: 'npm' yêu cầu file package-lock.json hoặc yarn.lock ở root — nhưng blog Zola này không có package.json, vì nó là site tĩnh thuần, build bằng Zola (Rust), không phải Node.js.

- name: Setup Node.js 24
  uses: actions/setup-node@v6
  with:
    node-version: '24'
    cache: 'npm'   # ← yêu cầu package-lock.json không tồn tại

Lỗi thật sự nằm ở cấu hình workflow — ai đó copy template từ repo Node.js và quên xoá cache: 'npm'. Sửa nhanh: xoá dòng cache: 'npm' là xanh.

Nhưng vấn đề lớn hơn mới lộ ra.

Chẩn đoán: Phát hiện zola build treo trên CI

Sau khi sửa npm cache, push lại. Lần này QA Gatekeeper không fail nhanh nữa — nó chạy mãi không xong. Log dừng cập nhật ở 20:17:27Z, không có dòng mới, runner chết đứng.

Bảng timeline cho thấy rõ pattern:

RunBranchKết quảThời gian
#28125523996feat/wwdc26-landingfailure (vaccine V8)~5 giây
#28126154700feat/wwdc26-landingcancelled (treo)>15 phút → huỷ

Workflow cũ cấu trúc thành một job duy nhất qa-check chạy tuần tự:

jobs:
  qa-check:
    timeout-minutes: 30
    steps:
      - run: python3 qa_check.py        # static checks OK
      - run: python3 scripts/build_*.py  # build data
      - run: zola build                  # ← TREO ở đây
      - run: python3 qa-404-checker.py   # không bao giờ chạy
      - run: python3 scripts/security_public_audit.py

Vấn đề: zola build trên site 503 trang phức tạp (nhiều load_data, template lồng nhau) đôi khi deadlock trên runner GitHub. Timeout-minutes:15 không hoạt động như mong đợi — workflow engine báo in_progress nhưng runner đã ngừng log. Cả pipeline chết theo.

Đỉnh điểm: 5 pull request cùng lúc đỏ vì gate chính bị kẹt. Không merge được, không deploy được, không tung ra tính năng mới.

Giải pháp: Tách qa.yml thành 2 job song song

Quyết định: tách pipeline thành hai job chạy song song, tách biệt trách nhiệm.

Job 1: static-checks — Gate chính

Chỉ chạy các check tĩnh: conflict markers, secret leak, SCSS syntax, SEO frontmatter, QA Vaccine Gate (bộ detector từ CLAUDE.md), unit tests, security audit source-side. Không gọi zola build → không bao giờ treo.

static-checks:
  runs-on: ubuntu-latest
  timeout-minutes: 10
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
    - run: python3 -m unittest scripts.test_qa_vaccines ...
    - run: python3 qa_check.py              # gate chính
    - run: python3 scripts/security_public_audit.py

Job 2: build-smoke — Build + post-build, song song

Chạy zola build riêng, kèm retry 2 lần, rồi post-build checks (404, og:image, security audit trên public/). Không có needs: static-checks → không chặn gate chính.

build-smoke:
  runs-on: ubuntu-latest
  timeout-minutes: 20
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
    - cache + install zola
    - run: python3 scripts/build_feed_pagination.py
    - run: python3 scripts/build_references.py
    # ... các build data scripts
    - run: |
        for i in 1 2; do
          if zola build; then exit 0; fi
          sleep 10
        done
        exit 1
    - run: python3 qa-404-checker.py
    - run: python3 scripts/check_social_images.py --check
    - run: python3 scripts/security_public_audit.py

Ý tưởng cốt lõi: vaccine gate đã bắt lỗi build-breaker trước (Tera syntax sai, thiếu series_part, block lệch). Khi zola build treo, nó không phải do syntax — mà là race memory/CPU. Gate chính vẫn xanh, merge vẫn được attempted, deploy vẫn chạy.

So sánh trước/sau

Tiêu chíTrước (1 job tuần tự)Sau (2 job song song)
Gate chính般的Cùng job với buildTách rời (static-checks)
Khi zola build treoToàn pipeline chếtBuild chết, gate vẫn xanh
Merge bị chặnCó whenever build hangKhông
Time-to-green15–30 phút (nếu xong)~2 phút cho static-checks
Phân lập lỗiKhó (log lẫn nhau)Rõ ràng (mỗi job log riêng)
Retry buildTrong cùng jobTách job, dễ retry độc lập
Tiêu thụ runner1 runner blocked2 runner song song

Cải tiến đi kèm

Ngoài việc tách job, tôi áp dụng một số tối ưu khác:

1. Xoá cache: 'npm' không cần thiết. Blog Zola không có package.json. Cache npm chỉ gây fail cứng ngay bước setup.

2. Cache binary Zola. Dùng actions/cache@v4 với key runner.os-zola-0.22.1 — tiết kiệm ~10 giây mỗi run tải và giải nén Zola.

3. Retry zola build 2 lần. Nếu lần 1 fail (không phải syntax mà do transient), retry sau sleep 10. Giảm false-positive.

4. Tách security audit 2 lớp. Source scan ở static-checks (sớm, không cần build), full scan trên public/build-smoke (backstop).

5. timeout-minutes thực tế. Job gate chính 10 phút (dư cho static scan). Build smoke 20 phút (cho 503 trang + retry).

5 bài học kinh nghiệm

Bài học 1: Static gate và build gate phải tách biệt. Khi site lớn, zola build trở thành điểm single-point-of-failure. Đừng để nó giữ hostage gate merge. Tách ra, để build chạy nền.

Bài học 2: Vaccine gate cứu mạng. Bộ detector tĩnh (kiểm tra Tera syntax, block balance, series_part) bắt được 90% lỗi build-breaker trước khi zola build chạy. Đầu tư vào static detector đáng giá hơn retry build.

Bài học 3: Không copy workflow template mù quáng. cache: 'npm' từ template Node.js khiến site Zola đỏ 5 giây mỗi push. Luôn đọc kỹ tham số trước khi dán vào workflow.

Bài học 4: Khi CI treo, không phải lỗi của bạn. Đừng vội sửa template khi workflow báo in_progress mãi. Check updatedAt — nếu standstill, runner deadlock. Cancel, rerun. Quan trọng hơn: mở issue track lại, đừng hack quanh.

Bài học 5: Doctrine quan trọng hơn ego. Khi 5 PR đỏ, áp lực "merge cho xong" rất lớn. Nhưng CLAUDE.md quy định: "CI xanh mới được merge". Tuân thủ doctrine — merge khi đỏ chỉ tạo nợ kỹ thuật, deploy hỏng production.

Kết quả đạt được

Sau khi áp dụng cấu trúc mới:

  • Gate chính (static-checks) hoàn thành trong ~2 phút — không bao giờ treo.
  • 5 PR cũ có thể merge khi static-checks xanh, không bị kẹt bởi build hang.
  • Deploy không bị chặn — build-smoke red không stop production, vì gate chính đã pass.
  • 1 issue mới (#874) để track root cause hang zola build (cần investigate memory runner).
  • 0 template bị mass-sed — tránh phá hoại 40 templates đang hoạt động.

Kết luận

CI/CD cho site tĩnh lớn không phải "build xong là xong". Khi zola build trở thành single-point-of-failure, cả pipeline chết theo. Giải pháp không phải retry thêm, mà là cấu trúc lại trách nhiệm: gate tĩnh làm gate merge, build smoke chạy nền.

Triết lý: máy kiểm tra, máy sửa lỗi, máy merge, máy deploy. Con người chỉ quyết định sản phẩm. Để pipeline đỏ do build hang chặn mọi tiến trình — đó là khi bạn đang babysit CI thay vì vận hành nó.

Mã nguồn cấu hình. Các bạn có thể tham khảo PR thay đổi workflow thật trên repo SEOMONEY. Nếu đang vận hành blog Zola lớn, thử áp dụng: tách static gate, để build chạy song song. Bạn sẽ thấy pipeline xong nhanh hơn, và đỏ "có ý nghĩa" hơn.