Skip to content

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, /playbooks cù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 trong packages/observability/dsn.ts cò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úcnode scripts/smoke-production.mjs --local api www com hoặc --local all
Thử đúng một bản Pagesnode scripts/smoke-production.mjs --local www --deploy-url https://<hash>.ww3-conan-school.pages.dev
Mẫu tự kiểm, không mạngnode 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_dirKiểm
api.conan.schoolCổ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.schoolCổ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-hotfixCổ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.schoolCổ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.schoolTê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-routerCổ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.schoolNhư 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/frontendCổ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.schoolChỉ 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/backendGET 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.schoolkudo.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-planemcp.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/diagnostics trả 503 khi D1 không trả lời. Trước đó nó trả 200 kèm connection_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èm request_id.
  • /api/system/health nó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 đọc status), đổi giá trị thành not_measured / not_checked, thêm kind: "liveness", release và 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.mjs chưa vào repo, hay Access Bypass cho /__release củ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ì:

  1. Đọ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.
  2. Chạy lại tại máy: node scripts/smoke-production.mjs --local <alias> để xem production lúc này.
  3. 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.

  1. Thêm một mục vào TARGETS trong scripts/smoke-production.mjs, khoá là app_dir của workflow deploy, kèm alias ngắn. Dùng lại khối có sẵn: simplePages(url), accessPages(url), shell, jsonCheck, wwwPage, sameBuildGate.
  2. 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.
  3. 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.
  4. 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ồi sourcemaps upload dưới release = SHA, bằng npx @sentry/[email protected] ghim bản. Sau đó, luôn luôn, bước "Không publish source map" xoá mọi *.map trong 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-maps như 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ằng wrangler deploy --dry-run --outdir (không gọi Cloudflare) chỉ để lấy index.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/cloudflare thì 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 200 trong _redirects, nên /assets/khong-co.js trả index.html với mã 200 (đo 14.10.2026), lại kèm Cache-Control: public, max-age=31536000, immutable vì _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 đòi content-type JavaScript và thân không phải HTML. (docs trả 404 đúng nghĩa, vì VitePress có trang 404 riêng.)
  • env khai ở một step thì if: của chính step đó không thấy, và ngữ cảnh secrets không dùng được trong if. Gác "có Sentry chưa" vì vậy nằm trong script của step. Đưa token lên env cấp job thì gọn hơn, nhưng npm ci và 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. LearningJourney dướ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 (commit 5ee050a0, 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à header X-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.ts né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.
  • /api/system/health và diagnostics trướ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ên admin-conan-school.pages.dev/assets/index-<hash>.js.map trả 1,6 MB JSON với sourcesContent đủ 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.dev của admin và mam không nằm sau Access (đo 14.10.2026): tên miền admin.conan.school / mam.conan.school chuyển tới Access, còn admin-conan-school.pages.dev và mam-conan-school.pages.dev trả 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.

Tài liệu nội bộ nền tảng Conan School.