Kiểm production sau deploy
Hai repo (16.10.2026)
Từ 16.10.2026 mục tiêu www.conan.school và www.conan.school/edge-hotfix đã gỡ khỏi script ở repo này cùng các hàm chỉ chúng dùng; www và workflow deploy của nó ở repo conan-web. Các mục về www dưới đây là lịch sử và là đặc tả để dựng bước kiểm tương ứng bên đó. Xem Hai repo.
Một lượt deploy xanh chỉ nói một điều: wrangler đã upload xong. Nó không nói trang còn có ruột, api còn nói chuyện được với D1, hay tên miền production đang phục vụ ĐÚNG bản vừa build. Cả ba đã từng sai trong khi CI vẫn xanh:
/courses,/books,/news,/playbookscùng hiện "đang được cập nhật" vì API 500 bị nuốt thành danh sách rỗng (08.09.2026).- play.conan.school đứng yên nhiều tuần vì mọi lượt deploy rơi vào nhánh preview, còn tên miền gắn vào production (
known-risks#play-domain-lech). - api đóng băng hai ngày ở bản 01.09 trong khi frontend vẫn ship (token thiếu D1 Edit).
Bước Kiểm production sau deploy (scripts/smoke-production.mjs) là bước cuối của mọi lượt deploy: nó hỏi thẳng production bằng GET ẩn danh, và đỏ khi production hỏng.
Trạng thái (14.10.2026)
- Kiểm production: chạy ở cả 15 workflow deploy (qua
_deploy-cloudflare.yml). - Hợp đồng API (
scripts/contract-live.mjs): chạy khi file có mặt; vắng thì bảng ghi "bỏ qua" và có::notice::, không im lặng. - Cổng release của admin-router: CHƯA BẬT. Cloudflare Access chặn GET ẩn danh trước khi Worker chạy, nên cần một ứng dụng Access Bypass cho
admin.conan.school/__release(người sở hữu tạo). Chưa có thì mỗi lượt deploy admin-router ghi "bỏ qua" kèm::notice::: bản Worker vừa deploy chưa được kiểm. - Sentry (release + source map): CHƯA BẬT. Chưa có secret
SENTRY_AUTH_TOKEN, DSN trongpackages/observability/dsn.tscòn rỗng. Mỗi lượt deploy in::notice title=Sentry chưa bật::cho tới khi bật. Xem mục Sentry trong đường deploy.
Chạy ở đâu
| Chỗ | Lệnh |
|---|---|
CI, bước cuối của job deploy (timeout-minutes: 5, không cài gói nào) | node scripts/smoke-production.mjs (đọc APP_DIR, DEPLOY_TYPE, PROJECT_NAME, DEPLOY_URL, GITHUB_SHA) |
| Máy cá nhân, mọi lúc | node scripts/smoke-production.mjs --local api www com hoặc --local all |
| Thử đúng một bản Pages | node scripts/smoke-production.mjs --local www --deploy-url https://<hash>.ww3-conan-school.pages.dev |
| Mẫu tự kiểm, không mạng | node scripts/smoke-production.mjs --self-test (75 mẫu, cả đạt lẫn đỏ) |
--local chỉ GET ẩn danh với User-Agent ConanSmoke/1.0, không ghi gì, nên chạy từ máy không phạm luật "không deploy từ máy". Nó bỏ qua phép so release (máy không biết SHA nào đang được deploy).
Kết quả: một bảng trong Summary của lượt chạy, một ::error:: cho mỗi phép kiểm đỏ. Thông điệp annotation cố ý ổn định (không thời điểm, không id, không URL có hash), vì sổ lỗi tự động gom lỗi theo dấu vân tay của chính dòng đó.
Mỗi mục tiêu kiểm gì
Mục tiêu chọn theo app_dir của lượt deploy.
| app_dir | Kiểm |
|---|---|
api.conan.school | Cổng: /api/system/diagnostics trả release đúng SHA của lượt deploy (hỏi lại tới 150 giây). Rồi: diagnostics 200 với services.d1 = connected; /api/public/courses 200 JSON có ít nhất một khoá; HEAD ảnh bìa /api/public/ebooks/drink-book-01/cover.svg (bìa trong D1, từ 16.10.2026) trả 200 image/svg+xml; hợp đồng API qua scripts/contract-live.mjs nếu có. |
www.conan.school | Cổng: www.conan.school phục vụ đúng bộ asset băm của bản vừa deploy. Rồi, trên cả URL của bản deploy lẫn production: /, /courses, /books, /communities, /en trả 200, có <title>, <h1>, canonical, <main id="main-content"> và ít nhất 400 ký tự chữ SSR trong vùng đó (thanh điều hướng và chân trang không tính); /courses, /books, /communities không hiện câu trạng thái rỗng/lỗi và trỏ tới ít nhất 5/5/9 trang chi tiết từ dữ liệu (link trong <main>, /courses trừ 11 link viết cứng của LearningJourney); /en có lang="en"; không lộ khoá i18n; /sitemap.xml là sitemap index; /robots.txt khai Sitemap và không chặn toàn site. Production còn phải có header X-Conan-Edge-Version. |
www.conan.school/edge-hotfix | Cổng: header X-Conan-Release của bridge bằng SHA của lượt deploy (Worker tự khai bản đang chạy). Rồi: X-Conan-Edge-Version bằng EDGE_VERSION trong mã (kiểm phụ; đọc không được thì "không kết luận"), trang chủ qua bridge có ruột, X-Conan-Edge-Cache có mặt, /_next/* trả 410 (luật của bridge còn chạy). |
com.conan.school | Cổng: tokyo.conan.school phục vụ đúng bản vừa deploy. Rồi trên cả hai URL: trang khung 200 và asset JS chính là JavaScript thật; /api/public/courses qua proxy Pages Function trả JSON có khoá; đường cũ của ảnh hero lịch (thư mục /images/page-heroes/, trước khi sang R2) trả 301 tới đúng URL R2 (200 nghĩa là luật SPA nuốt đường ảnh, xem repo-slim). Thêm: HEAD hai ảnh image.conan.school/site/tokyo/... trả 200 image/*; đường bìa cũ /images/book-covers/so-tay-quay-do-uong.svg trả 301 tới api.conan.school/api/public/ebooks/covers/... (bìa sách vào D1, repo-slim bước 1b). |
admin.conan.school, mam.conan.school | Tên miền production chuyển tới trang đăng nhập dac2205.cloudflareaccess.com (hoặc 401/403 kèm Cloudflare-Access): site còn được bảo vệ, chỉ thế. 200 không qua Access là đỏ. Không bao giờ thử đăng nhập. Bằng chứng về bản deploy là URL của bản Pages: trang khung và asset JS thật, hoặc cũng sau Access. |
admin-router | Cổng: admin.conan.school/__release trả { service: "admin-router", release } với release bằng SHA. Access đứng trước Worker, nên chưa có ứng dụng Access Bypass cho đường đó thì dòng này là bỏ qua (::notice::), không phải đạt. Rồi: tên miền còn sau Access (chỉ kiểm bảo vệ). |
video2.conan.school | Như nhóm dưới, thêm: đường storyboard cũ trả 301 tới R2 trên cả hai URL, và một ảnh image.conan.school/site/video2/... trả 200 image/*. |
docs, studio, media, video, play/frontend | Cổng: production phục vụ đúng bản vừa deploy. Rồi: trang khung 200 có <title> và asset JS chính là JavaScript thật, trên cả hai URL. |
content.conan.school | Chỉ URL của bản deploy (tên miền đã thuộc Worker kudo). Chạy từ máy thì kiểm content-conan-school.pages.dev. |
play.conan.school/backend | GET api.conan.school/play/api/quizzes trả bộ đề: GET rẻ nhất chứng minh route /play/* tới đúng Worker, không chạm Durable Object, không mở phòng. |
kudo.conan.school | kudo.conan.school và content.conan.school trả 200 HTML hoặc một chuyển hướng. Origin kudo.thongphan.com hỏng thì Worker trả 502 và bước này đỏ: đó là production hỏng thật, dù lỗi không nằm ở repo này. |
conan-tool-plane | mcp.conan.school/health trả ok: true của conan-tool-plane. |
| app_dir lạ | ::notice:: rồi thoát 0. Thêm mục tiêu theo mục dưới. |
Hai URL: bản deploy và production
Với Pages, bước Deploy Pages ghi lại URL theo từng bản deploy (https://<hash>.<project>.pages.dev, đọc từ file kết quả có cấu trúc mà wrangler ghi qua WRANGLER_OUTPUT_FILE_PATH, rơi về dòng "Take a peek over at" trong log). Kiểm cả hai URL vì chúng trả lời hai câu khác nhau:
- URL của bản deploy trả lời "bản build này có đúng không". Nó không đi qua edge bridge, không qua bộ đệm, không phụ thuộc tên miền gắn vào đâu.
- Tên miền production trả lời "người dùng có đang thấy bản này không".
Cổng so bộ asset băm (/assets/*.js|css mà HTML tham chiếu) giữa hai URL. Vite và React Router đổi tên băm khi mã đổi, nên cùng bộ là cùng bản build. Đây là phép kiểm bắt đúng sự cố play: deploy rơi vào preview thì URL của bản deploy có bộ asset mới, production vẫn bộ cũ, và bước đỏ với lời ghi "bản deploy thuộc môi trường preview, không phải production".
Không lấy được URL của bản deploy thì bước Deploy Pages in ::warning::, và bảng kiểm ghi một dòng "không kết luận": chỉ production được kiểm, cổng so bản build không chạy.
Tham số ?smoke=: mỗi lượt gọi một giá trị riêng
Mọi GET vào tên miền production mang ?smoke=<sha7>-<nonce của lượt chạy>-<số thứ tự>. Một ?smoke=<sha> cố định không đủ, vì edge bridge của www lưu HTML theo NGUYÊN URL (tươi 60 giây, dùng tạm tới 24 giờ):
- Lượt hỏi đầu của cổng tới trước khi alias production kịp đổi thì nhận bản cũ, và chính bản cũ đó vào cache dưới URL ấy. Các lượt hỏi sau trong 60 giây nhận lại bản cũ từ cache, rồi bản STALE. Cổng chờ hết giờ trong khi production đã đúng từ lâu.
- Chạy lại job trong 24 giờ thì lượt đầu nhận bản STALE của lần chạy trước.
release = SHA
Mọi Worker deploy với --var CONAN_RELEASE:<sha>. Ba Worker trả nó ra ngoài: api ở /api/system/diagnostics (trường release) và /api/system/health; edge bridge của www ở header X-Conan-Release của mọi phản hồi (đóng dấu ở lối ra, không nằm trong bản lưu cache, nên một HIT vẫn nói bản đang chạy); admin-router ở /__release. So release với SHA của lượt deploy là cách duy nhất biết bản mới đã chạy, không chỉ đã upload. Một hằng số tăng bằng tay (như EDGE_VERSION) không làm được việc đó: người sửa quên tăng là bản cũ "đạt" ngay lượt hỏi đầu. Sentry dùng cùng giá trị làm release; trình duyệt nhận nó qua VITE_SENTRY_RELEASE ở bước Build.
Hai sửa ở api đi kèm (14.10.2026):
/api/system/diagnosticstrả 503 khi D1 không trả lời. Trước đó nó trả 200 kèmconnection_failed: <thông điệp gốc của D1>: mọi monitor chỉ nhìn mã trạng thái thấy xanh đúng lúc D1 sập, và thông điệp lỗi của D1 lộ ra công khai. Giờ thông điệp chỉ vào log, kèmrequest_id./api/system/healthnói thật. Nó từng trảmemory: "attached",db_connection: "waiting_on_pool", hai chuỗi viết cứng không đo gì nhưng nghe như phép kiểm thật. Giữ tên trường (có thể có monitor ngoài repo đọcstatus), đổi giá trị thànhnot_measured/not_checked, thêmkind: "liveness",releasevàchecks: "/api/system/diagnostics".
Không kết luận ≠ đạt ≠ đỏ
- Trang thách đố của Cloudflare (403/429/503 có
cf-mitigated: challenge, hoặc trang "Just a moment") là không kết luận:::warning::, không làm đỏ. WAF chặn máy chạy của GitHub không nói gì về bản deploy. Nhưng 403 thường, hay 500 có chữ "challenge" trong thân, vẫn là lỗi thật (có mẫu tự kiểm). - Site sau Access: dòng "còn sau Cloudflare Access" đạt khi site được bảo vệ, và chỉ nói điều đó: Access trả lời trước khi Pages hay Worker phía sau chạy. Script không bao giờ mang thông tin đăng nhập.
- Bỏ qua (
::notice::): chỉ dùng cho thứ chưa tồn tại hoặc chưa bật, ví dụscripts/contract-live.mjschưa vào repo, hay Access Bypass cho/__releasecủa admin-router chưa khai. Notice lặp lại ở mỗi lượt deploy cho tới khi bật: một phép kiểm chưa chạy phải nói ra, không được trông như đạt.
Chỉ báo, không rollback
Bước này không tự lùi bản. Rollback tự động trên một tín hiệu mới dựng là cách nhanh nhất để một phép kiểm sai đánh sập một bản đúng, và lùi api mà không lùi migration D1 là tạo ra đúng chiều lệch nguy hiểm (mã cũ đọc schema mới). Đỏ ở đây thì:
- Đọc bảng trong Summary của lượt chạy; sổ lỗi đã mở (hoặc mở lại) issue nhãn
error-ledger. - Chạy lại tại máy:
node scripts/smoke-production.mjs --local <alias>để xem production lúc này. - Sửa hoặc revert bằng commit, push, để CI deploy. Không
wrangler rollback, không deploy từ máy.
Thêm một mục tiêu
Chỉ sửa script, không sửa workflow: sửa _deploy-cloudflare.yml kích lại cả 15 workflow deploy.
- Thêm một mục vào
TARGETStrongscripts/smoke-production.mjs, khoá làapp_dircủa workflow deploy, kèmaliasngắn. Dùng lại khối có sẵn:simplePages(url),accessPages(url),shell,jsonCheck,wwwPage,sameBuildGate. - Phép kiểm mới cần hàm thuần thì thêm mẫu vào
scripts/smoke-production.selftest.mjs(chạy bằng--self-test; tách riêng để file chính không chạm ngưỡng 1.000 dòng của qc-scan): ít nhất một mẫu phải ĐỎ, và mẫu đỏ phải dựng giống trang THẬT (có thanh điều hướng, chân trang, khối viết cứng), không phải một mảnh HTML trơn. Ba phép kiểm của bản đầu xanh trên mẫu trơn mà không thể đỏ trên trang thật, xem mục bẫy dưới. node scripts/smoke-production.mjs --local <alias>phải xanh trên production hiện tại. Đỏ thì hoặc production đang hỏng thật, hoặc phép kiểm sai: sửa trước khi merge, đừng merge một gác đỏ sẵn.- Thông điệp lỗi không chứa thời điểm, id hay URL có hash.
Sentry trong đường deploy
Bốn secret của repo, người sở hữu tạo: SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_PROJECT_WEB (conan-web), SENTRY_PROJECT_WORKERS (conan-workers). Thiếu token thì mọi bước Sentry in ::notice:: và thoát 0; có token mà thiếu org/project thì ::warning::. Lỗi tải lên cũng chỉ là ::warning::: thiếu map thì stack trace khó đọc, site vẫn đúng, nên không chặn deploy, nhưng phải kêu.
- Pages: sau Build, nếu có token và build sinh
.map:sentry-cli sourcemaps inject(gắn debug id vào file JS trước khi deploy) rồisourcemaps uploaddưới release = SHA, bằngnpx @sentry/[email protected]ghim bản. Sau đó, luôn luôn, bước "Không publish source map" xoá mọi*.maptrong thư mục output: source map là toàn bộ mã nguồn kèm chú thích. - Worker: KHÔNG dùng
wrangler deploy --outdir dist --upload-source-mapsnhư hướng dẫn của Sentry. Cờ đó gửi map lên Cloudflare trong cùng lượt deploy, map có trần riêng 15 MB nén, map của api hôm nay ~10 MB thô (~2 MB nén) và lớn theo mỗi file JSON nội dung mà worker import: ngày vượt trần thì cái hỏng là deploy. Thay vào đó bước "Sentry source map (Worker)" bundle lần hai bằngwrangler deploy --dry-run --outdir(không gọi Cloudflare) chỉ để lấyindex.js.map, rồi tải lên dưới release = SHA. index.js của dry-run trùng từng byte với bản deploy (đã so 14.10.2026). Không gắn debug id được (bản deploy thật không mang đoạn inject), Sentry ghép map theo release +~/index.js. Worker nào không khai@sentry/cloudflarethì bước này bỏ qua.
Bẫy đã gặp khi dựng
- Pages trả 200 cho asset không tồn tại. tokyo và play khai
/* /index.html 200trong_redirects, nên/assets/khong-co.jstrảindex.htmlvới mã 200 (đo 14.10.2026), lại kèmCache-Control: public, max-age=31536000, immutablevì_headersáp cho cả/assets/*. Một HTML trỏ tới asset đã mất trông "200" ở mọi nơi. Phép kiểm asset đòicontent-typeJavaScript và thân không phải HTML. (docs trả 404 đúng nghĩa, vì VitePress có trang 404 riêng.) envkhai ở một step thìif:của chính step đó không thấy, và ngữ cảnhsecretskhông dùng được trongif. Gác "có Sentry chưa" vì vậy nằm trong script của step. Đưa token lênenvcấp job thì gọn hơn, nhưngnpm civà mọi script build (mã bên thứ ba) đọc được nó.?smoke=<sha>cố định tự đầu độc cache của chính nó, xem mục trên.- Bốn phép kiểm không thể đỏ (rà soát 14.10.2026, trước khi merge). Cả bốn xanh trên production và xanh trên mẫu tự kiểm, nhưng không đỏ được trong đúng sự cố chúng được dựng ra để bắt:
- Ngưỡng chữ SSR đếm cả chân trang. Phần đo bắt đầu ở
<main id="main-content">nhưng chạy tới cuối tài liệu, mà chân trang của www nằm sau</main>với 443 ký tự nhãn (đo 14.10.2026). Một trang chỉ còn<h1>vẫn "đạt" với hơn 450 ký tự. Giờ đo đúng vùng<main>...</main>(đếm cả<main>lồng), và thiếu<main id="main-content">là đỏ, không rơi về đo cả trang. /courses"đủ 5 link chi tiết" nhờ link viết cứng.LearningJourneydưới lưới khoá học có 11 link/courses/<slug>viết cứng, nên API trả 200 với một khoá vẫn ra 12 link. Giờ chỉ đếm link trong<main>và trừ slug đọc từapp/components/LearningJourney.tsx; trang còn dùng component đó mà không đọc được slug thì đỏ.- Cổng của edge bridge so
EDGE_VERSION, một hằng số tăng bằng tay. Lần sửa bridge 08.10.2026 (commit5ee050a0, thêm 301 cho playbook gộp) không tăng nó, nên với cổng cũ bản bridge cũ "đạt" ngay lượt hỏi đầu. Giờ cổng là headerX-Conan-Release= SHA. - Kiểm admin-router bằng cú chuyển hướng của Access. Access trả lời trước khi Worker chạy:
worker.tsném lỗi ở mọi request thì GET ẩn danh vẫn thấy đúng cú chuyển hướng đó. Giờ dòng Access ghi rõ "chỉ kiểm bảo vệ", và bằng chứng về bản deploy là/__release, "bỏ qua" có tiếng cho tới khi có Access Bypass.
- Ngưỡng chữ SSR đếm cả chân trang. Phần đo bắt đầu ở
/api/system/healthvàdiagnosticstrước 14.10.2026: một cái bịa giá trị, một cái trả 200 khi D1 hỏng. Cả hai trông như phép kiểm sức khoẻ.- Mã nguồn admin tải được công khai qua source map (đo 14.10.2026): admin build với
sourcemap: true, nênadmin-conan-school.pages.dev/assets/index-<hash>.js.maptrả 1,6 MB JSON vớisourcesContentđủ 58 file (packages/admin-shared/src/...), không qua Access. Tên miền admin nằm sau Access nên không ai thấy. Bước "Không publish source map" chặn từ lượt deploy admin kế tiếp, nhưng không gỡ map ở các bản cũ: Pages giữ mọi deployment cũ ở URL<hash>.admin-conan-school.pages.dev. Gỡ hẳn cần xoá các deployment cũ của project (việc của người sở hữu, ở dashboard Pages). - Alias production
*.pages.devcủa admin và mam không nằm sau Access (đo 14.10.2026): tên miềnadmin.conan.school/mam.conan.schoolchuyển tới Access, cònadmin-conan-school.pages.devvàmam-conan-school.pages.devtrả khung SPA cho bất kỳ ai. Khung không có dữ liệu (cả hai là SPA, dữ liệu đi qua api có gác riêng), nên đây chưa phải lỗ, nhưng mọi ranh giới quyền phải nằm ở API, không được dựa vào "trang admin chỉ mở được sau Access".
Xem thêm: Quy tắc triển khai, Sổ lỗi tự động, skill smoke-production.