# sub-cicd-cum — verify deploy cụm W3-W6 (YC-025)

**Vai:** cicd-monitor (READ-only) · **Range:** `0149f106..b275893c` (4 commit code) · **Ngày:** 2026-08-10
**Spawn:** 19:47 (VN) — kịp giờ chụp PRE trước khi deploy land.

---

## Stage 1 — Push + path filter

- `git log origin/main..HEAD` = **rỗng** (đã push hết).
- HEAD repo = `80febec9 [CLAUDE] Docs: sổ yêu-cầu +YC-025` — diff `b275893c..80febec9` = **1 file `docs/governance/so-yeu-cau-chu-du-an.md`** ⇒ khớp glob `docs/**` + `**/*.md` ⇒ **KHÔNG trigger CI** (gotcha #41). Xác nhận bằng ĐO chứ không suy: bảng `tasks` KHÔNG có row nào head_sha `80febec9`.
- ⇒ **gotcha #86 (push-đè nuốt deploy) KHÔNG áp** ở lượt này: run CUỐI-có-code CHÍNH LÀ run của `b275893c`.
- Commit trong range: `e7f282fc` (wal) · `af5f5be8` (W3 seed 7 HD-*) · `ad9a21ed` (W4+W6BE guard) · `b275893c` (W5+W6FE).
- Files 0149f106..b275893c = **28** (9 BE incl. DbInitializer + 8 FE + 3 test + docs/STATUS + 7 run-trace/WAL).
- `git diff 0149f106..b275893c -- '*Migrations*'` = **RỖNG** ⇒ 0-mig, kỳ vọng Mig prod GIỮ nguyên (72).

## PRE snapshot (đo LIVE 19:49, trước khi deploy land)

| App | index.js | index.css |
|---|---|---|
| admin | `index-DeBPqp_c.js` | `index-Desb-yZr.css` |
| eoffice | `index-CSIiQrhk.js` | `index-BGUiinrQ.css` |

⇒ KHỚP mốc PRE đề bài (`DeBPqp_c` / `CSIiQrhk`) — mốc chưa lão hoá, differential POST sẽ có răng.

---

## Chân 1 — Gitea Actions run `b275893c` → ✅ **PASS**

- Task id **595** = run_number **#482**, `head_sha b275893c8f`, `status: success` (poll 8 iter × ~48s; `conclusion` = `None` như thường lệ ⇒ tin `status`, luật MEMORY Stage-2).
- Bắt đầu 19:47:08 → xong 19:55:35 ⇒ **8m27s** (⚠️ chạm ngưỡng escalate >8min; xu hướng gần đây 5m3x-6m3x — ghi nhận, chưa phải lỗi).
- URL: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/482
- **Test gate (log web-UI 18.755 B, đối chiếu TÁCH-PHẦN):**
  - `SolutionErp.Domain.Tests.dll` → `Failed: 0, Passed: 45, Skipped: 0`
  - `SolutionErp.Infrastructure.Tests.dll` → `Failed: 0, Passed: 635, Skipped: 0`
  - Tổng **680** = **ĐÚNG kỳ vọng đề bài (680)**. Delta so baseline #444 (644 = 45+599): **+36**, TRỌN VẸN nằm ở Infra (Domain giữ 45) — khớp 3 file test trong diff (`AwV2DesignerMenuKeyTests` + `KhkkGroupMenuSeedTests` + `ContractWorkflowServiceApproveV2Tests`).

## Chân 2 — Bundle rotate 2 app → ✅ **PASS (4/4 artifact)**

| App | PRE (19:49) | POST (19:56) | Rotate | Size log CI |
|---|---|---|---|---|
| admin `.js` | `DeBPqp_c` | **`BZ1PG34E`** | ✅ | `1,813.77 kB` |
| admin `.css` | `Desb-yZr` | **`DVHtHLex`** | ✅ | `88.39 kB` |
| eoffice `.js` | `CSIiQrhk` | **`0ObPwWFB`** | ✅ | `1,728.76 kB` |
| eoffice `.css` | `BGUiinrQ` | **`Ibv3QD_1`** | ✅ | `94.02 kB` |

Hash live (index.html) **khớp từng ký tự** với dòng `dist/assets/index-<hash>` in trong log CI ⇒ đúng artifact của run #482, không phải bản cũ.

**Control-âm tầng-2 (bundle cũ đã bị GHI ĐÈ):** GET hash PRE giờ rơi về SPA-fallback —
`admin/assets/index-DeBPqp_c.js` → 200 **`text/html` 900 B** · `eoffice/assets/index-CSIiQrhk.js` → 200 **`text/html` 876 B**.
(Bẫy #69/S72: 200 ở đây KHÔNG phải "file còn sống"; discriminator là `Content-Type`.)

**Ship-proof tầng đĩa (mạnh hơn hash-delta, luật #69):** trên VPS chỉ còn **ĐÚNG 1** `index-*.js` mỗi app —
`fe-admin\assets\index-BZ1PG34E.js` = **1.813.772 B** @ 19:53:58 · `fe-user\assets\index-0ObPwWFB.js` = **1.728.765 B** @ 19:55:08.
Size khớp log CI **tới từng byte** (`1,813.77 kB` / `1,728.76 kB`), LM **trong cửa-sổ deploy** (19:47→19:55).

---

## Chân 3 — SEEDER W3 chạy THẬT trên prod (#51) → ✅ **PASS (4/4 mục a-d)**

Đo bằng **3 nguồn ĐỘC LẬP** (tam-giác #434): `sqlcmd -E` ⟂ Serilog boot ⟂ API-materialize-qua-EF.

### (a) `ApprovalWorkflows` họ `HD-*` — kỳ vọng 7 row type-3 IsActive=1 → **ĐÚNG 7/7**

| Code | ApplicableType | IsActive | IsUserSelectable | Version | Steps | Levels |
|---|---|---|---|---|---|---|
| HD-DV | 3 | 1 | 1 | 1 | 3 | 11 |
| HD-GK | 3 | 1 | 1 | 1 | 3 | 11 |
| HD-MB | 3 | 1 | 1 | 1 | 3 | 11 |
| HD-NCC | 3 | 1 | 1 | 1 | 3 | 11 |
| HD-NT-DV | 3 | 1 | 1 | 1 | 3 | 11 |
| HD-NT-NCC | 3 | 1 | 1 | 1 | 3 | 11 |
| HD-TP | 3 | 1 | 1 | 1 | 3 | 11 |

Đếm gộp: `HD_total=7 · type3=7 · active=7`.
🔑 **Đo thêm Steps/Levels chứ không chỉ đếm row** — vì seeder có nhánh `if (wf.Steps.Count == 0) LogWarning("tạo RỖNG (0 Bước có người)")`: 7 row RỖNG vẫn ra "7" ở phép đếm mà quy trình vô dụng. **3 Bước / 11 Cấp mỗi mã** ⇒ roster PRO→CCM→BGĐ ráp được người thật, KHÔNG rỗng.

### (b) `MenuItems` key `AwV2_Hd%` — kỳ vọng 7 → **ĐÚNG 7**

`AwV2_HdDV · AwV2_HdGK · AwV2_HdMB · AwV2_HdNCC · AwV2_HdNTDV · AwV2_HdNTNCC · AwV2_HdTP` — tất cả `ParentKey=ApprovalWorkflowsV2`, `IsVisible=1`.
Khớp **by-construction** với `MenuKeys.ContractWorkflowMenuKeys` (sinh từ `ContractApprovalWorkflowFamily.KeyToken`): `HD-TP→TP`, `HD-NT-NCC→NTNCC` ✅.

### (c) `Permissions` KHÔNG FK-nổ → **sạch 2 chiều**

- `Perm_AwV2_Hd` = **7** · **OrphanPerm = 0** (0 row Permissions trỏ tới MenuKey không tồn tại) · **MenuNoPerm = 0** (0 menu-row `AwV2_Hd*` mồ côi permission).
- ⇒ **invariant T6 giữ** (key vào `All` mà thiếu menu-row = FK nổ lần boot đầu — không xảy ra).
- ℹ️ **7 chứ không phải 91 (7×13 role) là ĐÚNG THIẾT KẾ, không phải thiếu sót** — log tự khai: `[INF] Seeded 7 admin permissions`. Designer leaf = admin-only, cấp cho 1 role. (Khác họ `Khkk%` 56 menu/728 perm vì họ đó cấp cho cả 13 role — 2 họ khác chủ đích, không phải drift.)
- **App pool SỐNG + restart trong cửa-sổ:** `w3wp PID 6012` ⇄ `appcmd list wp` → pool **`SolutionErp-Api`**, `StartTime = 19:55:13` (map PID→pool BẮT BUỘC, gotcha S167 — VPS chạy 6 w3wp của 2 dự án).
- **Log boot SẠCH:** dòng đầu `19:55:16.139 [INF] Applying migrations...` (chứng restart thật, chân (i) luật 3-chân-kiềng) → `ERR]/FTL]/FOREIGN KEY/SqlException` = **0 dòng**. **Control-dương cùng phép Select-String**: cùng file vẫn trích ra hàng chục dòng `[INF]`/`[WRN]` ⇒ **thước sống, 0-hit là vắng THẬT** (không phải grep gãy).
- Toàn bộ `[WRN]` còn lại là **pre-existing lành tính**: EF global-query-filter (họ Contract/PE), `nv.cao/nv.truong not found`, admin dùng password mặc định, DataProtection in-memory key. **KHÔNG có dòng "tạo RỖNG"** — khớp (a).

### (d) CONTROL — 8 workflow `KHKK-N%` GIỮ NGUYÊN → **8/8 nguyên vẹn**

`KHKK_N_total=8 · active=8`, tất cả `ApplicableType=10`, mỗi mã **3 Steps / 11 Levels** — **y hệt trước W3** ⇒ refactor builder chung (`AddProCcmBodStepsToWorkflow` dùng chung cho cả 2 họ) **KHÔNG phá** họ cũ.

### Anchor + control-âm hiệu chuẩn thước

| Chỉ số | Trước (baseline #438-#447) | Nay | Delta | Đọc |
|---|---|---|---|---|
| MenuItems | 200 | **207** | +7 | đúng 7 leaf mới |
| Permissions | 2221 | **2228** | +7 | đúng "Seeded 7 admin permissions" |
| Roles | 13 | **13** | 0 | không đụng role |
| sys.tables | 97 | **97** | 0 | 0-mig, không bảng mới |

- **Mig prod top** = `20260806070242_AddMaHangMucToContractSigningPlanLines` (**Mig 72**) — **GIỮ NGUYÊN**, khớp `git diff 0149f106..b275893c -- '*Migrations*'` = rỗng và khớp repo (`ls Migrations/*.cs` trừ Designer/Snapshot = **72**, row cuối cùng tên).
- **Control-âm:** key bịa `AwV2_Zz%` → **0** ⇒ mệnh đề `LIKE` không khớp-bừa, các số 7 ở trên là đo thật.

### Nguồn thứ 3 — API materialize qua EF (không phải đọc DB, không phải đọc log)

`GET /api/approval-workflows-v2` (bearer admin) → nhóm `types`:
- `applicableType=3 "Hợp đồng"`: `active=QT-HD-V2-001` (sample S29, giữ) + **`activeOthers` = 7 mã ĐÚNG BỘ** `[HD-DV, HD-GK, HD-MB, HD-NCC, HD-NT-DV, HD-NT-NCC, HD-TP]`, **isUserSelectable = 7/7** ⇒ Drafter pick được ở màn tạo HĐ.
- `applicableType=10 "Kế hoạch ký kết HĐ"` (**control**): `active=KHKK-N7` + `activeOthers` 7 mã còn lại = **8 nguyên** ✅.

⚠️ **Tự-đính-chính trong lượt:** phép đo đầu của tao ra `HD-* = 0` — **là ARTIFACT PARSE của tao**, không phải lỗi sản phẩm: response shape là `{types:[{active, activeOthers, history}]}`, script đoán `items/versions` nên rơi vào nhánh fallback đếm số key = 1. Đo lại đúng shape mới ra 7. (Ghi lại vì đúng lớp "KHÔNG ĐO ĐƯỢC phải tự nghi" — suýt báo FAIL oan.)

---

## Chân 4 — Smoke → ✅ **PASS (9/9)**

| Phép | Kỳ vọng | Thực đo |
|---|---|---|
| `GET https://admin.solutions.com.vn/` | 200 | **200** `text/html` |
| `GET https://eoffice.solutions.com.vn/` | 200 | **200** `text/html` |
| `GET /api/contracts?type=1` **ẩn danh** | 401 | **401** ✅ |
| `GET /health/ready` | 200 | **200** |
| login `admin@` | 200 | token 468 ký tự |
| `GET /api/contracts?type=1` (admin) | 200 | **200** |
| `GET /api/approval-workflows-v2` (admin) | 200 | **200** |
| `GET /api/menus` (admin) | 200 | **200** |
| `GET /api/contract-signing-plans` (admin) | 200 | **200** |
| **CTRL-ÂM** `GET /api/zzz-no-such-route` | 404 | **404** ⇒ 200 ở trên không phải catch-all |

**Bonus — không hồi-quy quyền role thường (gotcha #44/#85):** `test.drafter@solutions.com.vn` (bộ SỐNG ở `docs/HANDOFF.md` slot 64, KHÔNG dùng `nv.test@` đã chết) login OK (token 524) → `/api/contracts?type=1` **200** · `/api/approval-workflows-v2` **200** ⇒ guard 4-cửa W4 **không** đóng nhầm role thường.
⚠️ Lần đo đầu bị **MSYS path-conversion** bẻ chuỗi `-w` (`/api/...` → `C:/Program Files/Git/api/...`, `\n` → `/n`); đã đo lại với `MSYS_NO_PATHCONV=1` — số trên là bản sạch.

---

## Chân 5 — DLL marker `ContractWorkflowGuards` → ✅ **PASS**

`C:\inetpub\solution-erp\api\SolutionErp.Application.dll` (LM **19:48:48**, 2.480.640 B — trong cửa-sổ run):

| Khung | Marker đích | Control-DƯƠNG | Control-ÂM |
|---|---|---|---|
| **utf8** | `ContractWorkflowGuards` = **4** ✅ | `ContractSigningPlanWorkflowGuards` = 3 | `ContractWorkflowGuardz` = 0 |
| unicode | 0 (đúng bản chất: tên type nằm ở `#Strings` heap = UTF-8) | — | — |

⇒ **≥1 thoả**, và đợt trước control-âm = 0 nên đây là differential **0 → 4** thật.

**Bonus Domain.dll — và một bài học về THƯỚC:** literal `AwV2_Hd`/`HD-TP`/`HD-NT-NCC` ban đầu ra **0-hit ở CẢ utf8 lẫn utf16** ⇒ trông như "chuỗi không có trong DLL đã ship". **SAI — do CƠ CHẾ:** literal C# nằm ở `#US` heap dạng **UTF-16 lệch byte lẻ**; `Encoding.Unicode.GetString(bytes)` đọc từ offset 0 sẽ giải mã trật khung.
Đo lại **2 khung offset**:

| offset | AwV2_Hd | HD-TP | HD-NT-NCC | CTRL+ `AwV2_KhkkN1` | CTRL− `HD-ZZZ` |
|---|---|---|---|---|---|
| 0 | 0 | 0 | 0 | **0** ← control-dương CŨNG 0 ⇒ thước gãy | 0 |
| **1** | **1** | **1** | **1** | **2** | **0** |

🔑 Control-dương `AwV2_KhkkN1` (chắc chắn có trong prod từ S166) **cũng 0 ở offset 0** ⇒ đó là **thước gãy**, không phải chuỗi vắng. Cùng họ bẫy "0-hit sai do cơ-chế regex" (#440 backtick) — **luật: 0-hit chỉ được tuyên khi control-dương CÙNG PHÉP ra dương.**

---

## Tổng kết

| # | Chân | Verdict |
|---|---|---|
| 1 | Gitea run `b275893c` #482 success + gate **680** (45+635) | ✅ PASS |
| 2 | Bundle rotate 2 app (4/4 artifact + byte-exact + LM + control-âm) | ✅ PASS |
| 3 | Seeder W3 prod: 7 HD-* / 7 menu / FK sạch / KHKK-8 nguyên | ✅ PASS |
| 4 | Smoke 9/9 + anon 401 + role-thường không hồi-quy | ✅ PASS |
| 5 | DLL marker `ContractWorkflowGuards` = 4 (0→4) | ✅ PASS |

**VERDICT = PASS 5/5.** 0 blocker. 0 hành động sửa (vai READ-only, không đụng gì).

### Ghi chú không-chặn (cho em main, không phải lỗi)
1. **Deploy 8m27s** — chạm ngưỡng escalate >8min của baseline (gần đây 5m3x-6m3x). Nguyên nhân hợp lý: gate lên 680 (+36 test). Theo dõi lượt sau, chưa cần hành động.
2. **Đường log Serilog trong MEMORY sai** — ghi `C:\inetpub\solution-erppi\logs\` (chuỗi bị `\a` nuốt); đường THẬT = `C:\inetpub\solution-erp\api\logs\solution-erp-YYYYMMDD.log`. Đã sửa trong MEMORY lượt này.
3. **Admin prod vẫn dùng password mặc định** (`[WRN]` mỗi lần boot) — nợ bảo mật cũ, ngoài phạm vi đợt này.

== HẾT ==

