CICD-K1: PASS — 6/6 mục

# cicd-monitor — verify deploy K1 SP-002 (`50e6d8c`)

> Artifact chống #53: ghi TỪNG mục NGAY khi đo xong. Kết bằng dòng `END … VERDICT=`.
> Spawn 2026-07-31 · commit `50e6d8c38283a9dd45da2001bd2fc6596595558f` · READ-only (0 ghi prod)

## Stage 1 — Push + path-filter (PRE-điều-kiện)

- `git log -1` → `50e6d8c38283a9dd45da2001bd2fc6596595558f|[CLAUDE] Contract: K1 SP-002 danh muc loai HD - ContractCatalogEntries (Mig 70) + seed 86 + CQRS + FE kind-5 x2 app + 8 test|2026-07-31 15:45:50 +0700`
- `git log origin/main..HEAD` → **RỖNG** (đã push hết)
- `git diff --name-only 50e6d8c^ 50e6d8c` → **20 file** (neo bằng `X^..X` theo LUẬT S162 CẤM-`HEAD~1`, vì phiên có Stop-hook WAL auto-commit):
  BE 11 · FE 6 · Test 1 · Docs 2
- vs `paths-ignore` 7-glob: **18/20 file KHÔNG bị ignore** ⇒ CI **PHẢI chạy**. ✅

## Stage 1b — DỰ ĐOÁN TRƯỚC KHI ĐO (chống xác-nhận-ngược)

Đọc `DbInitializer.SeedContractCatalogAsync` (dòng 3163-3325) **trước** khi chạm prod:

- Parse lần 1 (regex line-anchored) → **78** ⇒ SAI (tuple xuống dòng). Vứt, KHÔNG báo cáo số này.
- Parse lần 2 (multiline) → **86**; cross-check độc-lập `raw code-literal` = **86**, distinct = **86**, dup = **[]**.
- Dự đoán `GROUP BY ApprovalGroup`: **1:5 · 2:10 · 3:5 · 4:23 · 5:14 · 6:15 · 7:13 · 8:1**
- Dự đoán `GROUP BY GroupCode`: 11 nhóm — A1:5 A2:1 A3:9 A4:5 B1:23 B2:14 B3:15 C1:6 C2:3 C3:4 CEO:1
- Seeder idempotent (`existingCodes.Contains(code) continue` + `IgnoreQueryFilters` né #75/#76) ⇒ deploy đầu `added=86`.
- Serilog dự kiến: `Seeded 86 contract catalog entries (S164 SP-002)` (chỉ ghi khi `added>0`).

## Bundle PRE-deploy (đo LIVE lúc run đang chạy, 15:47)

| App | js | css |
|---|---|---|
| admin | `index-B43R6Y17.js` | `index-BZ5F-g1w.css` |
| eoffice | `index-NLI5umBg.js` | `index-yNZDmVNz.css` |

Khớp nguyên văn hash đề-bài (`B43R6Y17` / `NLI5umBg`) ⇒ mốc "TRƯỚC" hợp lệ.

---

## ✅ Mục 1 — Gitea Actions run

- Run **id=547 · run_number=#434** · head_sha `50e6d8c3` · job `build-deploy` · runner `vps-vietreport-runner v0.2.13`
- created `2026-07-31T15:46:06+07:00` (push 15:45:50 → pick sau **16s**)
- Poll 7 iter × ~40s → iter7 `status=success`, updated `2026-07-31T15:52:37+07:00`
- **Duration ≈ 6m31s** · `conclusion=None` (tasks API không populate — tin `status`, đúng khuôn đã biết)
- URL: https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/434
- **PASS**

## ✅ Mục 2 — Test gate 598

Nguyên văn từ web-UI log (`/actions/runs/434/jobs/0/logs`, 18.698 B):

```
2026-07-31T08:46:34.2961110Z Passed!  - Failed:     0, Passed:    45, Skipped:     0, Total:    45, Duration: 196 ms - SolutionErp.Domain.Tes…
2026-07-31T08:49:26.8226799Z Passed!  - Failed:     0, Passed:   553, Skipped:     0, Total:   553, Duration: 1 m 44 s - SolutionErp.Infrastr…
```

- Domain **45** + Infra **553** = **598** — khớp baseline MỚI đề bài (590+8). Đối chiếu TÁCH-PHẦN: Domain **45 GIỮ NGUYÊN**, Infra **545→553 = +8** ⇒ đúng "7 lane K1 + T5b gate-fix" (delta rơi trọn vào Infra, đúng nơi file test mới nằm).
- 0 fail / 0 skip cả 2 project. `Failed!`/`error CSxxxx` = **0 hit**.
- ⚠️ Ghi chú phương-pháp: grep đầu tiên **0 hit** do format nhiều-khoảng-trắng (`Passed!  - Failed:     0`); đã soi lại format thật rồi mới trích — KHÔNG báo "không thấy dòng test".
- **PASS**

## ✅ Mục 3 — EF migration prod

```
20260731072441_AddContractCatalogEntries      ← TOP
20260729122015_AddContractSigningPlans
20260727033522_AddPeAllowApproverDelete
20260717032812_AddPeApprovedBudgetSnapshot
```

- `__EFMigrationsHistory` TOP == repo HEAD mig ⇒ **Mig 70 applied prod**.
- Chân-kiềng (i) chứng-minh-restart-thật: `w3wp` PID **2644 StartTime 7/31/2026 3:52:13 PM** (trong cửa-sổ deploy 15:46:06→15:52:37) + dòng ĐẦU Serilog `2026-07-31 15:52:19.770 +07:00 [INF] Applying migrations...` ⇒ mọi số DB dưới đây đo **SAU** recycle (né đúng lỗi #423).
- **PASS**

## ✅ Mục 4 — Seed prod 86 + phân bố 8 nhóm

`sqlcmd` (SQL-auth `vrapp`, sau recycle):

| Đo | Kết quả |
|---|---|
| `COUNT(*) FROM ContractCatalogEntries` | **86** |
| `GROUP BY ApprovalGroup` | **1:5 · 2:10 · 3:5 · 4:23 · 5:14 · 6:15 · 7:13 · 8:1** |
| `COUNT(DISTINCT Code)` | **86** (0 trùng) |
| `IsActive=1` | **86** · `IsDeleted=1` = **0** |
| `COUNT(DISTINCT GroupCode)` | **11** |

Serilog (`solution-erp-20260731.log`) nguyên văn:
```
2026-07-31 15:52:27.074 +07:00 [INF] Seeded 86 contract catalog entries (S164 SP-002)
2026-07-31 15:52:25.867 +07:00 [INF] Seeded 1 menu items
2026-07-31 15:52:26.030 +07:00 [INF] Seeded 1 admin permissions
```
- Control ÂM: `findstr "contract catalog KHONGTONTAI"` → **0 hit** ⇒ findstr có răng, không phải khớp bừa.
- Đối chứng: 2 dòng `Seeded 1 menu items` / `Seeded 1 admin permissions` = menu-row `ContractCatalog` + permission cũng vào (leaf `Catalogs` thứ 5).
- 🔑 **TAM-GIÁC 3 NGUỒN ĐỘC-LẬP khớp từng đồng**: source-parse (86 / 8 nhóm) == sqlcmd prod (86 / 8 nhóm) == **API materialize qua EF** (86 / 8 nhóm, xem Mục 6). Không nguồn nào suy ra nguồn kia.
- **PASS**

## ✅ Mục 5 — Bundle đổi ×2 app (#77 byte-level)

**Vế DƯƠNG — hash MỚI là asset THẬT:**

| URL | code | content-type | size |
|---|---|---|---|
| admin `/assets/index-CKgq0YlH.js` | 200 | `application/javascript` | **1.751.576** |
| admin `/assets/index-DD3xQhNt.css` | 200 | `text/css` | 86.298 |
| eoffice `/assets/index-CHMCU35C.js` | 200 | `application/javascript` | **1.667.782** |
| eoffice `/assets/index-CdXRNT7E.css` | 200 | `text/css` | 92.092 |

**Vế ÂM — hash CŨ phải BIẾN MẤT khỏi đĩa** (né SPA-fallback-200-trap S72, discriminator = `Content-Type` không phải status):

| URL cũ | code | content-type | size |
|---|---|---|---|
| admin `index-B43R6Y17.js` | 200 | **`text/html`** | 900 |
| admin `index-BZ5F-g1w.css` | 200 | **`text/html`** | 900 |
| eoffice `index-NLI5umBg.js` | 200 | **`text/html`** | 876 |
| eoffice `index-yNZDmVNz.css` | 200 | **`text/html`** | 876 |

⇒ cả 4 rơi về SPA-shell 876/900 B (trùng khít size index.html) = file cũ đã bị ghi đè thật, KHÔNG phải cache.

**Ship-proof `Last-Modified` in-window** (#69 TWO-MODE — hash-delta một mình không đủ):
- admin `/` → `Last-Modified: Fri, 31 Jul 2026 08:51:06 GMT` = **15:51:06 +07** ✔ in-window · `Cache-Control: no-cache`
- eoffice `/` → `Last-Modified: Fri, 31 Jul 2026 08:52:07 GMT` = **15:52:07 +07** ✔ in-window · `Cache-Control: no-cache`

**4/4 hash ROTATE + 4/4 hash cũ chết + LM in-window. PASS**

## ✅ Mục 6 — Smoke

| Check | Kỳ vọng | Đo được | KL |
|---|---|---|---|
| `GET /health` | (bare) | **404** | control — route bare không tồn tại |
| `GET /health/ready` | 200 | **200** | ✅ |
| `GET /health/live` | 200 | **200** | ✅ |
| `GET /api/catalogs/contract-catalog` **KHÔNG token** | 401 | **401** | ✅ authz sống |
| CONTROL `…/contract-catalog-KHONGTONTAI` | 404 | **404** | ✅ 401 CÓ NGHĨA |
| CONTROL `…/bia-dat-hoan-toan` | 404 | **404** | ✅ (route bịa ⇒ 401 ở trên không phải blanket) |
| `https://admin.solutions.com.vn/` | 200 | **200** | ✅ |
| `https://eoffice.solutions.com.vn/` | 200 | **200** | ✅ |
| **CÓ token admin** `GET …/contract-catalog` | 200 + 86 row | **200, 25.374 B, 86 row** | ✅ |

Cross-check EF-materialize (mạnh hơn sqlcmd vì đi qua Domain→Infra→Api thật):
```
rows materialized via EF = 86
by approvalGroup: 1:5 . 2:10 . 3:5 . 4:23 . 5:14 . 6:15 . 7:13 . 8:1
distinct codes = 86
sample row: code=A1-01 group=1 signer=TP.CCM
```
Class-level `[Authorize]` + ghi `[Authorize(Roles="Admin")]` (đọc `ContractCatalogController.cs:21-22,39-56`) ⇒ 401-không-token đúng thiết-kế, không phải #44 silent-403.
- **PASS**

---

## Ghi chú lệch / quan sát (KHÔNG phải FAIL)

1. **Hash CI ≠ hash local build.** Đề bài dự-báo local ra admin `BE7zE81C`/user `DSXJJbJq`; CI ra `CKgq0YlH`/`CHMCU35C`. Tiêu chí đề bài (ĐỔI so với trước + curl 200) **thoả**. Nghĩa: artifact CI khác byte với artifact local (thường do Node minor / cây npm resolve khác) — bình thường, nhưng ⇒ **đừng bao giờ dùng hash local làm mốc kỳ vọng cho prod**, luôn re-snapshot LIVE.
2. **Duration 6m31s** — dài nhất 4 run gần đây (#432 5m50s · #433 5m55s · #431 5m50s). Chưa tới ngưỡng escalate; hợp lý vì run này có thêm Mig 70 + seed 86 row + 8 test mới. Theo dõi tiếp, chưa cần hành động.
3. **`Passed!` grep 2-khoảng-trắng** — lần grep đầu 0-hit là DƯƠNG-GIẢ do format; đã soi format rồi mới kết luận (bài học: 0-hit phải kèm control-dương trước khi báo).
4. `[WRN]` global-query-filter (ContractSigningPlan/Contract ↔ Approval/Changelog/LevelOpinion) là warning EF **có sẵn từ trước** (Mig 69 S161), không do commit này sinh ra.

## Kết luận

Không có critical issue. Không đề xuất rollback. 0 thao tác ghi lên prod.

END sub-cicd-verify-k1 — VERDICT=PASS 6/6 (run #434 id=547 success 6m31s · gate 598=45D+553I · Mig 70 top prod · seed 86/8-nhóm khớp 3 nguồn · bundle 4/4 rotate + 4/4 hash cũ chết · smoke 8/8 + 401 có-nghĩa)
