Vấn đề: Khi “Tất cả trong một” trở thành rào cản

Chào mọi người, tôi là Thành. Sau khi chuyển blog từ Next.js sang Astro và triển khai lên Cloudflare Workers, mọi thứ ban đầu hoạt động rất mượt mà. Tuy nhiên, khi tần suất viết bài tăng lên, tôi nhận ra một thiết kế chưa hợp lý: Sự phụ thuộc quá chặt chẽ giữa Code và Content.

Toàn bộ nội dung bài viết (Markdown) nằm chung trong repo với logic xử lý, component và cấu hình hệ thống. Điều này dẫn đến hai vấn đề khó chịu:

  1. Git History hỗn loạn: Những commit “fix CSS” nằm xen kẽ với các bài viết mới, làm mất đi tính nguyên tử (atomicity) của lịch sử thay đổi.
  2. Workflow cồng kềnh: Để viết một bài blog đơn giản, tôi phải tương tác trực tiếp với repo code.

Tôi quyết định tách nội dung ra một repo riêng mang tên mon-blog-content và liên kết nó vào repo chính qua Git Submodule tại đường dẫn src/content. Ý tưởng thì đơn giản, nhưng nó lại làm gãy hệ thống CI/CD mặc định của Cloudflare.

Quá trình thử nghiệm: Giới hạn của sự “mặc định”

Thử nghiệm 1: Custom Build Command

Bước đầu tiên, tôi thử can thiệp vào lệnh build trong Cloudflare Dashboard:

git submodule update --init --recursive && pnpm install && pnpm build

Kết quả: Build thành công, nhưng hệ thống không thể tự động hóa hoàn toàn. Cloudflare chỉ lắng nghe thay đổi từ repo chính. Khi tôi push bài mới vào repo content, Cloudflare “không hay biết” gì để thực hiện rebuild.

Thử nghiệm 2: Bế tắc với Deploy Hook

Tôi tìm cách trigger rebuild từ repo content thông qua Webhook. Tuy nhiên, một hạn chế đáng tiếc là Cloudflare Workers hiện chưa hỗ trợ Deploy Hook như Cloudflare Pages. Không có cơ chế nào để một repo bên ngoài “ra lệnh” cho Workers build lại.

Kết luận: Để giải quyết triệt để bài toán tách biệt Code/Content và tự động hóa flow, tôi phải chuyển toàn bộ pipeline sang GitHub Actions.

Giải pháp: Pipeline chuyên nghiệp với GitHub Actions

1. Quản lý nội dung bằng Git Submodule

Repo mon-blog-content giờ đây chỉ chứa Markdown và tài nguyên liên quan. Repo mon-blog đóng vai trò là “khung xương” kỹ thuật. Hai thành phần này có vòng đời độc lập, giúp việc quản lý phiên bản trở nên cực kỳ trong sạch.

2. GitHub Actions thay thế Cloudflare CI

Thay vì để Cloudflare tự kéo code, tôi sử dụng GitHub Actions để thực hiện toàn bộ quy trình build. Điểm mấu chốt nằm ở bước checkout:

- uses: actions/checkout@v4
  with:
    submodules: recursive # Tự động kéo dữ liệu từ mon-blog-content
    fetch-depth: 0 # Lấy đầy đủ history để phục vụ versioning

Khi repo content có thay đổi, tôi sử dụng repository_dispatch để trigger repo chính chạy lại workflow.

3. Tự động hóa Versioning (Semver)

Tôi tự viết một script Bash nhỏ trong workflow để quản lý phiên bản theo đúng đặc thù của blog, tránh phụ thuộc vào các thư viện bên thứ ba:

IFS='.' read -ra P <<< "$CURRENT_VERSION"
M=${P[0]}; m=${P[1]}; p=${P[2]}
case "$VERSION_TYPE" in
  major) M=$((M+1)); m=0; p=0 ;;
  minor) m=$((m+1)); p=0 ;;
  patch) p=$((p+1)) ;;
esac

4. Kiểm soát Deploy qua GitHub Releases

Quy trình triển khai (deployment) giờ đây được kiểm soát chặt chẽ thông qua trạng thái của Release:

  • Push vào dev: Tự động tăng patch version, tạo tag kiểm thử.
  • Tạo Release: Tăng minor version, tạo GitHub Release ở dạng Draft.
  • Publish: Khi tôi nhấn “Publish” bản Draft, workflow mới chính thức deploy lên Cloudflare Workers.

Tổng kết

Việc rời bỏ CI/CD mặc định không phải vì Cloudflare không tốt, mà vì nhu cầu kiểm soát quy trình của tôi đã vượt quá những gì công cụ mặc định cung cấp. Chuyển sang GitHub Actions giúp tôi:

  • Checkout submodule linh hoạt.
  • Kiểm soát chính xác thời điểm và phiên bản deploy.
  • Tách biệt hoàn toàn luồng công việc của Code và Content.

Bây giờ, mỗi khi có cảm hứng, tôi chỉ cần viết Markdown, push lên repo content và mọi thứ sẽ tự động xuất hiện trên blog sau vài phút. Đôi khi, giải pháp tốt nhất không phải là công cụ xịn nhất, mà là công cụ cho phép bạn làm chủ hệ thống của mình tốt nhất.

Tài liệu tham khảo