Files
solution-erp/.claude/workflows/runs/2026-07-31-S164-4gd-khkk-fanout/sub-cicd-verify-k6.md
2026-08-01 12:11:32 +07:00

13 KiB
Raw Blame History

CICD-K6: PASS — 5/5

cicd-monitor — verify deploy K6 (S167, 2026-08-01)

Push range e202393..1c6d77a (2 commit: d82c5a8 wal-flush + 1c6d77a K6). ĐÚNG 1 file code fe-admin/src/components/Layout.tsx. 0-mig · fe-user 0-diff · BE 0-diff (đo: git diff --name-only e202393..1c6d77a -- fe-user/ src/Backend/ tests/ = 0 file). Run #440 (task id 553) — success, 11:54:38 → 12:01:07 (6m29s, trong dải quen 5m3x6m3x). Artifact ghi TỪNG MỤC ngay khi có bằng chứng (luật #53: ghi-đĩa-TRONG-lúc-làm; return chỉ là tóm tắt).


M0 — Mốc PRE (tự đo LIVE lúc run CÒN running, KHÔNG tin số chép)

Cửa sổ vàng: bắt được #440 ở running lúc 11:55:43 ⇒ bundle LIVE lúc đó = PRE thật.

App index-*.js index-*.css size js (B) Last-Modified js Content-Type
admin index-C2FhHCNT.js index-DqXm5Hpn.css (86.495) 1.758.685 01 Aug 04:13:05 GMT = 11:13:05 +07 application/javascript
eoffice index-DFOCQ2ks.js index-DlN3yZ5L.css (92.233) 1.676.752 01 Aug 04:14:05 GMT = 11:14:05 +07 application/javascript

🔑 PRE tự chứng là bundle #439 bằng LM, không chỉ bằng hash: LM 11:13:05 / 11:14:05 nằm TRỌN trong cửa-sổ deploy #439 (11:08:17 → 11:14:33). Trùng baseline đề bài — nhưng trùng đó là KẾT QUẢ ĐO, không phải giả-định đầu vào (bài "mốc PRE lão hoá sau MỖI deploy"). Content-Type application/javascript + ~1,7MB ⇒ không dính bẫy SPA-fallback (text/html ~900B). File tải về khớp byte-for-byte Content-Length.

M0-bis — Chọn byte-marker CÓ RĂNG (verify 0-hit PRE TRƯỚC khi phong)

Đề bài cảnh báo đúng: K6 = 12 key literal đã có từ K4a + comment sẽ bị Vite strip ⇒ marker ngây thơ = 0 bit. Đo PRE xác nhận từng cái:

LOẠI "key đơn lẻ" — PRE admin đã có sẵn: PurchaseEvaluations 4 · KeHoachKyKet 1 · HopDongCung 1 · PeWorkflows 3 ⇒ POST có hit cũng 0 bit. LOẠI tên biến ADMIN_HIDDEN_KEYS — minifier đổi tên; đo POST: source-name 0 hit, thực tế thành var IW=new Set([...]). (Bài K3 earlyFinalizeLocked tái xác nhận.) LOẠI comment lineageNƠI-CONFIG / thắng mốc trước: PRE 0 POST 0 ⇒ Vite strip đúng như dự đoán.

CHỌN marker HỢP-CHẤT: TRẬT TỰ KỀ NHAU của 12 literal trong new Set([...]). Từng literal thì cũ, nhưng thứ tự kề nhau chỉ sinh ra khi Set mới được minify. Lineage source: git grep ADMIN_HIDDEN_KEYS @e202393 = 0 hit (exit 1) · @1c6d77a = 2 hit ⇒ marker chưa tồn tại ở mốc PRE. Marker ÂM (K6 là DISPLAY-ONLY, không gỡ route): /purchase-evaluations, /khkk/list, /suppliers PHẢI CÒN trong bundle admin POST. Differential: eoffice kỳ vọng FROZEN (fe-user 0-diff) ⇒ marker phải 0 ở eoffice cả PRE lẫn POST.


Mục 1 — Run Gitea cho 1c6d77a PASS

head_sha 1c6d77a6ccd95b2e287a3602f23780558c9d1ecc (khớp HEAD; git log origin/main..HEAD rỗng)
run #440 / task id 553 / status=success
thời gian created 11:54:38 → updated 12:01:07 = 6m29s
tail log Deploy done. App pool started.API /health/live -> 200🏁 Job succeeded

paths-ignore (7-glob): 8/10 file trong range là .claude/** (bị bỏ qua), nhưng fe-admin/** KHÔNG ⇒ CI đúng ra phải chạy, và nó đã chạy.

Mục 2 — Gate CI 622 = 45 D + 577 I PASS (tách-phần, khớp từng vế)

Đo ở web-UI log …/actions/runs/440/jobs/0/logs (18.697 B):

Project dòng log Failed Passed Skipped
SolutionErp.Domain.Tests.dll L29 0 45 0
SolutionErp.Infrastructure.Tests.dll L47 0 577 0
Tổng 0 622 0
  • Khớp dự đoán TRƯỚC khi đo: giữ nguyên 622, delta 0 — đúng: K6 chỉ thêm Set + comment, 0 file test trong range.
  • control ÂM: grep -c 'Failed!' = 0; pattern lỗi = 1 hit duy nhất là dòng runner vô hại Failed to setup Pty Unsupported (L12, có ở mọi run).
  • Flaky đã khai DeleteContractSigningPlan_SoftDeletes: run này KHÔNG đỏ (Failed 0). Mốc #439 xanh, #440 xanh ⇒ 2 run liên tiếp sạch, chưa cần re-run.

Mục 3 — Mig GIỮ 71 + sys.tables 97 PASS (2 nguồn)

  • Nguồn 1 — git: git diff e202393..1c6d77a -- '*Migrations*' = rỗng ⇒ 0-mig phía code. Đếm file repo (filter neo-đuôi Designer\.cs$|ModelSnapshot\.cs$ — bản vá từ K5) = 71.
  • Nguồn 2 — sqlcmd prod (đo SAU status=success, tức sau recycle): MIGTOP=20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime (= Mig 71, GIỮ) · MIGCOUNT=71 · SYSTABLES=97 (is_ms_shipped=0).
  • Anchor DB không nhích (đúng cho FE-only): MENUITEMS=200 · PERMS=2221 — y hệt mốc #438.

Mục 4 — Bundle: admin ROTATE / eoffice FROZEN-hợp-lệ PASS

App asset PRE POST size LM POST (+07) trong cửa-sổ deploy?
admin js C2FhHCNT Ig1R5tAh ROTATE 1.758.685 → 1.758.853 (+168) 11:59:40 (11:54:3812:01:07)
admin css DqXm5Hpn DqXm5Hpn FROZEN 86.495 (giữ) 11:59:40
eoffice js DFOCQ2ks DFOCQ2ks FROZEN 1.676.752 (giữ) 12:00:41 (PRE là 11:14:05)
eoffice css DlN3yZ5L DlN3yZ5L FROZEN 92.233 (giữ) 12:00:41

Admin rotate = ĐẠT điều kiện FAIL-nếu-không của đề bài. Δ +168 B — dương, nhỏ, đúng cỡ 1 new Set 12 key backtick (~170 ký tự); comment bị strip nên không cộng thêm.

🔑 eoffice FROZEN là LÀNH TÍNH — chứng bằng 3 nhân chứng độc lập, KHÔNG phải "đoán vì fe-user 0-diff":

  1. LM ĐÃ ĐỔI 11:14:05 → 12:00:41, nằm trong cửa-sổ #440 ⇒ file có bị ghi lại (deploy.yml copy dist\* vô điều kiện = #69 mode-A), không phải bundle cũ kẹt lại. Đây là discriminator quyết định: bundle-cũ-kẹt sẽ giữ LM 11:14:05.
  2. Log CI in thẳng asset phát ra — L128 dist/assets/index-DFOCQ2ks.js 1,676.75 kB lúc 05:00:41.56 GMT ⇒ runner ĐÃ BUILD LẠI fe-user trong CHÍNH run này và Vite phát ra cùng hash. LM trên prod = mốc build này khớp tới từng giây. Frozen = build-determinism, không phải skip.
  3. cmp PRE vs POST eoffice = byte-identical ⇒ nhất quán với source 0-diff. (Đối chiếu admin: L93 dist/assets/index-Ig1R5tAh.js 1,758.85 kB @ 04:59:40.39 — khớp cả hash lẫn size lẫn LM prod. Log-CI là nhân chứng thứ 4, độc lập với curl prod.)

🔑 Control-ÂM tầng-2 (chứng ghi-đè thật): GET hash CŨ admin index-C2FhHCNT.js200 text/html 900B ⇒ rơi về SPA-fallback ⇒ bundle cũ đã biến mất khỏi đĩa. (200 ở đây KHÔNG phải "còn sống" — discriminator là Content-Type.)

4-bis — Byte-marker: 🔴 SUÝT FAIL-OAN 2 LẦN vì CƠ-CHẾ REGEX, không phải vì code

Marker hợp-chất đo lần đầu ra PRE 0 / POST 0 — đọc vội thì thành "K6 không ship" ⇒ FAIL. Không phong vội, đi soi byte thật (grep -oE '.{90}HopDongCung.{90}') thì thấy dạng minify THẬT:

var IW=new Set([`PurchaseEvaluations`,`KeHoachKyKet`,`Contracts`,`HopDongCung`,`Forms`,
`Reports`,`Master`,`Hrm`,`Off`,`Personal`,`Workflows`,`PeWorkflows`]);
  • Sai lần 1: regex của tao chỉ chấp ["'] — rolldown/Vite 8 phát backtick (template literal). 0-hit là do regex không thể khớp, KHÔNG phải do vắng mặt.
  • Sai lần 2: vá xong, truyền pattern qua biến trong "..." → backtick bị shell nuốt, grep -cE gãy (grep: Unmatched () và trả 0 gần như im lặng trong vòng lặp. Lại một 0-hit giả.
  • Chốt bằng grep -F fixed-string (không còn regex để sai):
Marker PRE admin POST admin POST eoffice kỳ vọng
Trọn chuỗi 12-key new Set([...]) 0 1 0 0→1, eoffice 0
Adjacency 3-key `PurchaseEvaluations`,`KeHoachKyKet`,`Contracts` 0 1 0 0→1
control-ÂM đảo trật tự `KeHoachKyKet`,`PurchaseEvaluations` 0 0 0 0
control-ÂM chuỗi bịa ZZZ_NOT_EXIST_MARKER 0 0 0 0

🔑 COUNT-DELTA — nhân chứng thứ 2, độc lập với adjacency. Vì literal đã tồn tại từ K4a nên sự có mặt = 0 bit, nhưng số lần xuất hiện tăng thì đo được. PRE→POST admin, cả 12/12 key đều +1:

PurchaseEvaluations 4→5 · KeHoachKyKet 1→2 · Contracts 7→8 · HopDongCung 1→2 · Forms 3→4 · Reports 3→4 · Master 4→5 · Hrm 28→29 · Off 400→401 · Personal 2→3 · PeWorkflows 3→4 · Workflows 9→11 (+2).

Con +2 duy nhất KHÔNG phải bất thường mà là kiểm-tra-nhất-quán tự thân: Workflows là substring của PeWorkflows, nên Set thêm 1 Workflows + 1 PeWorkflows ⇒ đếm substring ra +2. Nếu K6 chỉ ship một phần thì hình dạng 12×(+1) với đúng 1 ngoại lệ giải-thích-được này sẽ không dựng lại được.

🔑 Marker ÂM (display-only, KHÔNG phải gỡ route) — ĐẠT: /purchase-evaluations 68→68 · /khkk/list 13→13 · /suppliers 16→16. Route còn nguyên trong bundle admin ⇒ K6 cắt đúng lớp hiển thị, không lỡ tay cắt lớp routing.

Mục 5 — Smoke + phép K6-đặc-thù PASS

Bearer admin: POST /api/auth/loginaccessToken len 468

Endpoint code
GET /api/contracts 200
GET /api/purchase-evaluations 200
GET /api/menus 200
GET /api/contract-signing-plans 200
control-ÂM GET /api/khong-ton-tai-zzz (có bearer) 404
control-ÂM GET /api/contracts KHÔNG token 401

🔑 Phép K6-đặc-thù — 2 tầng độc lập (gotcha #82) đo được bằng số: GET /api/menus bearer admin trả 200 item (khớp MENUITEMS=200 sqlcmd ⇒ không mất row nào). Trong đó 10/10 root bị K6 ẩn VẪN được BE trả về: Contracts, Forms, HopDongCung, Hrm, KeHoachKyKet, Master, Off, Personal, PurchaseEvaluations, Reports — cộng cả 2 con của SystemWorkflows + PeWorkflows12/12. ⇒ Đúng như comment K6 tự khai: ẩn ở client, KHÔNG thu quyền. Deep-link + API vẫn 200. Muốn CHẶN thật phải làm ở BE authz.


Verdict

PASS 5/5. Run #440 1c6d77a success 6m29s · gate 622 = 45 D + 577 I (Failed 0, delta 0 đúng dự đoán) · Mig GIỮ 71 + sys.tables 97, MenuItems/Perms đứng im 200/2221 · admin ROTATE Ig1R5tAh (+168 B) & eoffice FROZEN-lành-tính (LM đổi + log CI in cùng hash + byte-identical) · marker 12-key Set PRE 0→POST 1 + count-delta 12/12 + marker-ÂM route 68/13/16 nguyên vẹn · smoke 4/4 + 2 control-âm + menu-tree 12/12 key vẫn được BE phục vụ.

Treo / khuyến nghị (không chặn):

  1. 🔴 Bài học to nhất run này: 0-hit marker bị SAI 2 lần liên tiếp do cơ-chế regex, cả 2 lần đều trông y hệt "code không ship". (a) rolldown/Vite 8 minify string → backtick, regex neo ["'] không bao giờ khớp; (b) truyền pattern chứa backtick qua biến trong "..." làm grep gãy và trả 0 gần như im lặng. ⇒ Từ nay: marker chuỗi trên bundle dùng grep -F fixed-string, và luôn trích byte thật quanh literal (.{90}X.{90}) trước khi kết luận vắng mặt.
  2. Kỹ thuật mới dùng được lại — COUNT-DELTA: khi literal đã tồn tại từ đợt trước (marker "có/không" = 0 bit), đo số lần xuất hiện PRE→POST. Ngoại lệ substring (WorkflowsPeWorkflows ⇒ +2) lại thành kiểm-tra-nhất-quán miễn phí.
  3. Log CI in thẳng dist/assets/index-<hash>.js <size> — nhân chứng thứ 4 cho ship-proof, độc lập hoàn toàn với curl prod, và là cách rẻ nhất phân biệt FROZEN-lành-tính với bundle-kẹt (so LM prod với mốc build in trong log).

Bundle baseline MỚI (post-#440): admin Ig1R5tAh/DqXm5Hpn · eoffice DFOCQ2ks/DlN3yZ5L. (Snapshot-per-run — lão hoá ngay ở deploy kế.)

END cicd-k6 — VERDICT=PASS 5/5