DevOps / 5 min read
Free Games Notifier: vận hành một notifier nhỏ bằng CI/CD
Một case study về cách biến notifier game miễn phí thành workflow vận hành: scheduled Actions, secrets, dev/prod routing, registration qua PR.
Free Games Notifier: vận hành một notifier nhỏ bằng CI/CD
free-games-notifier bắt đầu từ một nhu cầu đơn giản: nhận email khi có game miễn phí hoặc deal Steam đáng chú ý.
Nếu chỉ giải quyết phần “gửi email”, project có thể dừng ở một script Node.js chạy local. Nhưng để nó chạy đều đặn, dễ kiểm tra, không lộ credentials và không sửa dữ liệu vận hành bằng tay, cấu trúc xung quanh script quan trọng không kém phần fetch dữ liệu.
Vì vậy project được đóng gói như một workflow automation nhỏ: GitHub Actions chạy theo lịch, cấu hình được tách khỏi secrets, manual run có đường dev/prod riêng, registration đi qua repository_dispatch, và thay đổi subscriber được đưa vào pull request thay vì ghi thẳng vào default branch.
Bài toán
Core logic của notifier khá rõ:
- Lấy game miễn phí từ Epic Games Store.
- Lấy game miễn phí hoặc đang giảm giá từ Steam.
- Lọc theo ngưỡng giá phù hợp với từng subscriber.
- Render email HTML/plain text bằng EJS.
- Gửi email qua SMTP.
Phần vận hành mới là chỗ cần thiết kế cẩn thận hơn:
- Job cần chạy hàng ngày mà không phụ thuộc vào máy cá nhân.
- SMTP credentials không được nằm trong repository.
- Manual run phải test được mà không gửi nhầm cho production list.
- Subscriber registry cần có lịch sử thay đổi và review gate.
- Registration form static không được ghi trực tiếp vào data file.
- Khi job lỗi, cần có tín hiệu đủ rõ để xử lý.
Những ràng buộc này khiến project giống một pipeline vận hành nhỏ hơn là một cron script đơn lẻ.
Runtime: GitHub Actions
Workflow chính là Free Games Notifier. Nó chạy hằng ngày lúc 02:00 UTC và cũng hỗ trợ workflow_dispatch để chạy thủ công.
Manual run có input environment:
prod: dùng mailing list production mặc định.dev: overrideEMAIL_LIST_NAME=dev-usersđể test cùng code path nhưng không gửi cho subscriber thật.
Cách tách này giữ workflow gọn: không cần clone pipeline cho dev, không cần comment tạm logic gửi mail, và không cần đổi code chỉ để kiểm tra template hoặc config. Cùng một entry point, cùng một dependency graph, chỉ khác routing qua environment variables.
Workflow cũng giới hạn quyền manual dispatch cho repository owner. Với một automation có thể gửi email ra ngoài, trigger control là một phần của thiết kế, không phải chi tiết phụ.
Config và secrets
Subscriber data nằm trong src/data/email-lists.json. Đây không phải secret; nó là operational data. Đặt nó trong repo giúp thay đổi có diff, có lịch sử, và có thể đi qua review như các thay đổi code khác.
Tách ba lớp này làm project dễ vận hành hơn:
- secrets đổi được mà không chạm code;
- policy như retry, timeout, price cap đổi được theo environment;
- subscriber registry vẫn có audit trail.
Registration không ghi thẳng vào repository
Project có registration UI trong docs/, phù hợp để host bằng GitHub Pages. Form nhận:
- email;
steamMaxPriceVnd;- target list name.
GitHub Pages là static hosting, nên form không thể tự ghi vào src/data/email-lists.json. Flow được chia thành nhiều bước:
- User submit form từ GitHub Pages.
- Static page gửi JSON đến registration endpoint.
- Endpoint trigger
repository_dispatch. - GitHub Actions nhận event
register-subscriber. - Script validate payload và update
src/data/email-lists.json. - Workflow dùng
peter-evans/create-pull-requestđể mở PR.
Điểm quan trọng là bước cuối. Subscriber mới không âm thầm mutate default branch. Mỗi thay đổi trở thành một PR nhỏ, có branch riêng, diff rõ ràng, và có thể review trước khi merge.
Với một side project, flow này nhìn có vẻ nhiều bước. Nhưng đổi lại nó tránh được kiểu “public form ghi trực tiếp vào repo data”, vốn khó audit và khó rollback khi có payload lỗi.
PR automation
Workflow Subscriber Registration PR giữ phần ghi dữ liệu ở một vùng hẹp:
- validate
client_payloadtrước khi chạy update; - setup Node.js 20;
- chạy
npm ciđể dependency reproducible; - gọi
scripts/apply-subscriber-registration.js; - tạo PR chỉ với thay đổi trong
src/data/email-lists.json.
Script update subscriber có behavior xác định:
- tạo recipient mới nếu chưa tồn tại;
- update price cap nếu email đã có trong list;
- noop nếu payload không tạo ra thay đổi thực tế.
Các case này được cover bằng node:test, vì đây là phần dễ làm hỏng dữ liệu vận hành nhất. Network call tới Epic/Steam có thể thay đổi theo thời gian, nhưng rule nội bộ về normalize email, price cap và upsert subscriber cần ổn định.
Failure path và redeploy
Notifier có retry config cho request và gửi failure email cho admin list khi job lỗi. Đây là phần nhỏ nhưng cần có nếu workload chạy không có người ngồi nhìn terminal.
Project cũng có workflow Redeploy GitHub Pages. Workflow này được trigger thủ công, cập nhật docs/deploy.json, commit marker mới, rồi push lại default branch để ép GitHub Pages rebuild.
Deploy marker là một cơ chế đơn giản: không cần sửa nội dung thật chỉ để kick lại static deployment, nhưng vẫn để lại lịch sử commit cho thao tác vận hành đó.
Những gì được test
Test suite tập trung vào các rule có ảnh hưởng trực tiếp đến vận hành:
- normalize recipient entry;
- tính max Steam price cap;
- lọc deal theo ngưỡng của từng user;
- render text email với price cap;
- xử lý quoted CI secret values;
- normalize registration payload;
- upsert subscriber vào mailing list.
Phạm vi test không cố bao trùm mọi thứ. Nó ưu tiên những điểm nếu sai sẽ gây spam, gửi thiếu người nhận, ghi sai subscriber data, hoặc làm manual run khó tin cậy.
Tóm lại
free-games-notifier là một project nhỏ, nhưng nó có đủ các mảnh của một workflow vận hành nghiêm túc:
- scheduled runtime;
- manual dispatch có dev/prod routing;
- secrets tách khỏi operational data;
- registration qua event bridge;
- data changes qua pull request;
- retry và failure notification;
- tests cho các rule có rủi ro vận hành.
Không cần biến side project thành một hệ thống enterprise để chứng minh cách nghĩ về CI/CD. Chỉ cần một bài toán nhỏ, nhưng để các đường chạy, đường lỗi và đường thay đổi dữ liệu được thiết kế rõ ràng.