# sub-cicd — verify vá GỐC gotcha #92 (`305b573f`)

> Ghi TRONG LÚC LÀM (chống #53). File này là sản phẩm chính; return chỉ là tóm tắt.
> Bắt đầu: 2026-08-13 01:15 +07 (2026-08-12T18:15Z)

## Đề bài
Run QUYẾT ĐỊNH: chứng minh fix `npm ci` + lockfile-committed + GUARD-92 có làm build **deterministic** không.
Kỳ vọng hash CI == local đã verify runtime lành:
- fe-user `index-CR538vZx.js` **1.700.709 B**
- fe-admin `index-C10ta-wB.js` **1.780.807 B**

---

## Stage 1 — Push + filter (ĐO)

```
305b573f5abe85b9f6bbd640efa58db36a5950eb | 2026-08-13 01:13:34 +0700
  [CLAUDE] CICD: vá GỐC #92 — npm ci theo lockfile đã commit + guard React-đơn-bản
c03955405deb69c2 | 01:01:29 | Docs: P0-2 ... [skip ci]
7ebfc8c746f52e5d | 00:45:17 | Docs: S191 ... [skip ci]
git log origin/main..HEAD  => RỖNG (đã push hết)
```
Diff `305b573f`: 3 file — `.gitea/workflows/deploy.yml` (+61/-…), `fe-admin/package-lock.json` (+176), `fe-user/package-lock.json` (+176). **0 file BE · 0 migration · 0 file docs-only** ⇒ KHÔNG rơi vào 7-glob paths-ignore ⇒ PHẢI có run.

## Stage 2 — gotcha #91 detector (updated_at vs branch-head) + #86 (push sát nhau)

| Nguồn | Giá trị | Kết luận |
|---|---|---|
| `/repos/{r}`.updated_at (đọc DB tầng app) | `2026-08-13T01:13:54+07:00` | **≥ giờ push 01:13:34** ⇒ tầng app ĐÃ thấy push |
| `/branches/main`.commit.id (đọc git trên đĩa) | `305b573f…` @ 01:13:34 | khớp HEAD local |
| ActionRun sinh ra | task **id 601 / run_number 488**, head_sha `305b573f`, created `01:13:51` | ⇒ #91 **XANH**, không tái diễn ca S189 |

**#86 (đọc run CUỐI của range):** `c0395540` + `7ebfc8c7` mang `[skip ci]` ⇒ **KHÔNG sinh task nào** (task 600 = `091073f9` từ 08-12 22:11). ⇒ run #488 là run duy nhất & cuối của range. Không có ca push-đè nuốt deploy.

## PRE-snapshot prod (chụp 01:15 +07, LÚC run đang `running` — deploy ở CUỐI job nên đây là trạng thái TRƯỚC deploy)

| App | index từ HTML | Content-Length | Content-Type | Last-Modified |
|---|---|---|---|---|
| eoffice | `index-CR538vZx.js` / `index-DNE0buTg.css` | 1.700.709 | application/javascript | **Wed, 12 Aug 2026 18:10:28 GMT** (=01:10:28 +07) |
| admin | `index-C10ta-wB.js` / `index-CTIZKsjE.css` | 1.780.807 | application/javascript | **Wed, 12 Aug 2026 18:10:37 GMT** (=01:10:37 +07) |

🔴 **Cảnh báo phương-pháp (ghi TRƯỚC khi đo, không phải bào chữa sau):** prod HIỆN ĐANG chạy **chính bản cứu chép tay** = đúng 2 hash kỳ vọng. Nên **hash-equality sau deploy KHÔNG phân biệt được** "CI ra đúng hash rồi ship" ⟂ "deploy không chạy, file cũ nằm nguyên". ⇒ Bằng chứng CHÍNH phải là:
1. dòng `dist/assets/index-….js` **trong log CI** (nguồn độc lập với prod), và
2. `Last-Modified` prod **nhảy vào cửa sổ deploy** (> 18:13:51 GMT) — chứng file bị GHI ĐÈ thật.
Hai chân này cùng xanh mới kết luận được "deterministic".

---

## Stage 0 — server-health guard (runbook S189)
`ssh vietreport-vps "fsutil volume diskfree C:"` → **free 26.241.798.144 B (24,4 GB)** / 54,9 GB ⇒ **XANH** (ngưỡng đỏ <5GB). Disk-death S189 KHÔNG tái.

## Stage 3 — Test gate (log CI, đọc TÁCH-PHẦN)

```
Passed!  - Failed: 0, Passed:  45, Skipped: 0, Total:  45 — SolutionErp.Domain.Tests.dll (227 ms)
Passed!  - Failed: 0, Passed: 652, Skipped: 0, Total: 652 — SolutionErp.Infrastructure.Tests.dll (2 m 49 s)
```
45 + 652 = **697** — khớp **CHÍNH XÁC** canonical `docs/STATUS.md:477` row Tests = 697 (45 Domain + 652 Infra). **Δ0** — đúng hình dạng diff: commit chỉ đụng `deploy.yml` + 2 lockfile, **0 file test**, nên gate đứng yên là kỳ vọng, không phải trùng hợp.

## Stage 5 — Migration (đo trước, commit 0-mig)
- Đĩa: `ls Persistence/Migrations/*.cs` trừ Designer/Snapshot ⇒ **MIGCOUNT=73**, đuôi = `20260812141501_AddContractEndedByLevelFinalize`.
- `git diff --name-only 091073f9..305b573f -- '*Migrations*'` ⇒ **0 file** ⇒ commit **0-mig** ⇒ kỳ vọng prod `__EFMigrationsHistory` top **KHÔNG nhích**, vẫn `20260812141501_AddContractEndedByLevelFinalize`. (Verify prod ở Stage 5-verify bên dưới.)

## 🔴 Stage 4a — GUARD-92 CÓ CHẠY (log CI, trích nguyên văn)

```
2026-08-12T18:21:28.7680899Z GUARD-92 OK: react don ban = 19.2.5     <- fe-admin
2026-08-12T18:22:47.1308510Z GUARD-92 OK: react don ban = 19.2.5     <- fe-user
```
**2/2 app** in dòng OK, đúng 1 bản React `19.2.5`. **0 hit `GUARD-92 FAIL`** ⇒ không có nhánh đỏ nào bị nuốt. Kèm `npm ci` + `added 239 packages` cho **cả 2** app (bằng nhau ⇒ 2 cây cùng kích thước, khớp lockfile đã commit).

## 🔴 Stage 4b — PHÉP ĐO QUYẾT ĐỊNH: hash CI vs local

Dòng build **trong log CI** (nguồn ĐỘC LẬP với prod):
```
dist/assets/index-CTIZKsjE.css     88.85 kB      (fe-admin)
dist/assets/index-C10ta-wB.js   1,780.80 kB      (fe-admin)
dist/assets/index-DNE0buTg.css     94.66 kB      (fe-user)
dist/assets/index-CR538vZx.js   1,700.70 kB      (fe-user)
```

| Artifact | Local (đã verify runtime lành) | CI run #488 | Khớp? |
|---|---|---|---|
| fe-user JS | `index-CR538vZx.js` 1.700.709 B | `index-CR538vZx.js` 1,700.70 kB | ✅ **hash + size** |
| fe-user CSS | `index-DNE0buTg.css` 94.665 B | `index-DNE0buTg.css` 94.66 kB | ✅ |
| fe-admin JS | `index-C10ta-wB.js` 1.780.807 B | `index-C10ta-wB.js` 1,780.80 kB | ✅ **hash + size** |
| fe-admin CSS | `index-CTIZKsjE.css` 88.858 B | `index-CTIZKsjE.css` 88.85 kB | ✅ |

**4/4 khớp.** Vite hash = hash NỘI DUNG ⇒ hash trùng nghĩa là **cây phụ-thuộc resolve ra y hệt**, dù runner **node v24.15.0/npm 11.12.1** ⟂ local **node v22.13.0/npm 10.9.2**. ⇒ Giả thuyết "còn nguồn bất định thứ 2 ở chênh-lệch node/npm" **BỊ BÁC bằng đo**, không phải bằng suy luận. Nguồn bất định DUY NHẤT là việc xoá lockfile — đã vá.

## Stage 4c — Run kết thúc

Task **601 / run_number 488**, head `305b573f`, `status=success`, created `01:13:51` → updated `01:24:15` (+07) ⇒ **10m24s** (chậm hơn baseline ~6m: Infra tests 2m49s + `npm ci` ×2). Log CI cuối:
```
18:23:23  Wrote appsettings.Production.json
18:23:23  Deploy done. App pool started.
18:23:38  API /health/live -> 200
18:24:13  🏁  Job succeeded
```
0 dòng `error`/`FAIL` trong toàn log (18.055 B). Chỉ 2 warning lành tính có sẵn từ trước: chunk >500 kB + `INEFFECTIVE_DYNAMIC_IMPORT realtime.ts`.
URL: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/488

## Stage 4d — SHIP-PROOF trên prod (3 chân)

**Chân 1 — Last-Modified nhảy vào cửa sổ deploy** (đây là chân BẮT BUỘC, vì hash PRE đã trùng hash kỳ vọng nên hash-delta vô dụng ở lượt này):

| Asset | LM PRE (01:15) | LM POST | Trong cửa sổ run 18:13:51-18:24:15Z? |
|---|---|---|---|
| eoffice JS+CSS | 18:10:28Z | **18:23:17Z** | ✅ ghi đè THẬT |
| admin JS+CSS | 18:10:37Z | **18:21:58Z** | ✅ ghi đè THẬT |

**Chân 2 — Content-Length prod == local == log CI, tới BYTE:**
`1.700.709` (user JS) · `94.665` (user CSS) · `1.780.807` (admin JS) · `88.858` (admin CSS). `Content-Type` = `application/javascript` / `text/css` (KHÔNG phải SPA-fallback).

**Chân 3 — CONTROL ÂM (chứng thước còn sống, chống bẫy SPA-fallback 200):**
```
GET /assets/index-D6lFjgKF.js  (bundle crash S191) -> 200  text/html  876 B
GET /assets/index-KHONGTONTAI.js (hash bịa)        -> 200  text/html  900 B
```
⇒ mọi hash KHÔNG tồn tại đều rơi về shell 876-900B. Bundle thật trả 1,7MB `application/javascript` ⇒ phép đo phân biệt được. **Bản crash `D6lFjgKF` đã biến mất khỏi đĩa prod.**

**Chân bổ sung — shell không stale** (đúng lỗ trắng-màn S88): `index.html` LM cũng nhảy vào cửa sổ, `Cache-Control: no-cache`, `<script type="module" src="/assets/index-CR538vZx.js">` trỏ đúng bundle vừa ship, `/dashboard` → 200 cả 2 app.

## Stage 5-verify — Migration prod (commit 0-mig)

```
sqlcmd -S .\SQLEXPRESS -d SolutionErp -E
  TOP3: 20260812141501_AddContractEndedByLevelFinalize
        20260806070242_AddMaHangMucToContractSigningPlanLines
        20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime
  MIGCOUNT=73    TABLES=97
```
Prod top == đuôi repo == `20260812141501_AddContractEndedByLevelFinalize`; MIGCOUNT prod **73** == đếm đĩa **73**; `sys.tables` **97** unchanged. ⇒ đúng kỳ vọng NO-MIG (history KHÔNG nhích + diff `*Migrations*` rỗng).

## Stage 4e — Smoke API

| Check | Kỳ vọng | Thực đo |
|---|---|---|
| POST /api/auth/login (admin) | token | ✅ `accessToken` len=468 |
| GET /api/contracts | 200 | ✅ 200 |
| GET /api/purchase-evaluations | 200 | ✅ 200 |
| GET /api/menus | 200 | ✅ 200 |
| GET /api/contract-signing-plans | 200 | ✅ 200 |
| GET /api/contracts **không bearer** (control âm) | 401 | ✅ 401 |
| /health/ready | 200 | ✅ 200 |

## 🔴 Stage 6 — SMOKE RUNTIME (luật #92(1)) + GIỚI HẠN PHÉP ĐO

**Bằng chứng mạnh nhất tao có — byte-identity với bản ĐÃ verify runtime:**
```
SHA256 prod eoffice/assets/index-CR538vZx.js = 37ce5a68…4307
SHA256 local fe-user/dist/assets/index-CR538vZx.js = 37ce5a68…4307   ⇒ TRÙNG
SHA256 prod admin/assets/index-C10ta-wB.js  = 18be9949…8c12
SHA256 local fe-admin/dist/assets/index-C10ta-wB.js = 18be9949…8c12  ⇒ TRÙNG
```
Bản local này = bản đã **mở trình duyệt thật, console 0 lỗi** trước khi commit (ghi trong commit-msg `305b573f`). Prod đang chạy **cùng một chương trình, từng byte** ⇒ tính-chất-chạy-được **chuyển giao được**.

🔴 **Vì sao lần này khác 2 lần trước (phải nói rõ, không tô hồng):** S189/S191 cũng "byte-match" — nhưng là byte-match **CI ⟂ prod**, mà **cả hai đầu đều CHƯA ai chạy thử**. Hai file giống hệt nhau vẫn cùng crash. Lần này một đầu của phép so **đã được người chạy thật** ⇒ chuỗi mới có neo. Đó là khác biệt về CHẤT, không phải về độ chặt.

🔴 **Giới hạn tao tự khai — file-integrity KHÔNG thay được runtime-check:**
- Tao **KHÔNG** tự mở được trình duyệt vào prod sau deploy (không có công cụ browser trong tay lượt này). Tao chỉ chứng minh **byte trên prod == byte đã được verify**, KHÔNG phải "tao thấy nó chạy".
- Grep dấu hiệu React trong bundle đã ship: `react.dev/errors`=2 · `SECRET_INTERNALS`=3 · `react.transitional`=3 · marker signalr=12 — **giống hệt nhau ở cả 2 app**. Nhưng tao **KHÔNG có control âm** cho phép đo này (bundle hỏng `D6lFjgKF` đã bị xoá khỏi prod, không lấy được bản đối chứng) ⇒ **các số này chưa hiệu chuẩn, tao KHÔNG dùng nó làm bằng chứng**, chỉ ghi lại làm mốc cho lần sau.
- **Lỗ còn lại:** 1 lần load thật `https://eoffice.solutions.com.vn` + `https://admin.solutions.com.vn` bằng mắt/trình duyệt = đóng trọn. Chi phí ~30 giây, và nó chính là thứ đã để lọt 2 lần.

## Kết luận

Fix **deterministic ĐẠT** — chứng bằng đo, không suy: cùng lockfile, runner (node v24.15.0/npm 11.12.1) build ra **đúng 4/4 artifact** trùng hash + trùng size với local (node v22.13.0/npm 10.9.2). Nghi-vấn "chênh node/npm là nguồn bất định thứ 2" **bị bác bằng dữ liệu**. GUARD-92 chạy thật trên cả 2 app, in `react don ban = 19.2.5`.

**VERDICT = CICD: PASS 6/6** (run+head · gate 697 · GUARD-92 2/2 · hash CI==local 4/4 · mig 73 no-advance · ship-proof 3 chân + smoke 7/7) — kèm **1 caveat khai rõ**: runtime-check độc lập trên prod chưa chạy (byte-identity là proxy mạnh nhưng KHÔNG tương đương).

END sub-cicd-va-goc-92 VERDICT=PASS 6/6 (caveat: runtime-check doc lap chua chay)


