CICD-K2: PARTIAL — 5/6 mục (mục 4 = CHÂN-LÝ-RỖNG, không phải sạch) # CI/CD verify K2 SP-002 — commit `0779f2d` > Artifact chống #53 — ghi TỪNG MỤC NGAY KHI ĐO. Vai: cicd-monitor (READ-only). Ngày: 2026-07-31. > Deploy KHỎE, KHÔNG khuyến nghị rollback. Điểm trừ duy nhất = **phép đo mục 4 không có răng** > (bảng rỗng), không phải lỗi deploy. --- ## Mục 0 — Push + path-filter (tiền đề) ``` $ git log -1 --format='%H|%s|%ci' 0779f2d45a932ec7c279c191d9ae3fe3505442af|[CLAUDE] Contract: K2 SP-002 phieu KHKK mang hang-muc + nhom duyet N1-8 (Mig 71) + FE x2 app + 11 test|2026-07-31 17:05:27 +0700 $ git log origin/main..HEAD --oneline (RỖNG — đã push trọn) ``` `git diff --name-only 0779f2d^..0779f2d` = **23 file**: BE 9 · Mig 71 ×3 file (3-file rule đủ) · FE 8 (admin ×4 + user ×4 đối xứng) · test 3 · paths-ignore 2 (`docs/STATUS.md`, `.claude/skills/ef-core-migration/SKILL.md` — KHÔNG chặn vì ≥1 file code). Neo bằng SHA tuyệt đối `0779f2d^..0779f2d` (LUẬT S162: cấm `HEAD~1`, Stop-hook WAL auto-commit). ## Mục 0b — SNAPSHOT PRE-MIGRATE (đo lúc run #435 còn `running`, 17:07) ``` MIGTOP=20260731072441_AddContractCatalogEntries <- Mig 70 (K1), Mig 71 CHƯA áp PLANS=0 · LINES=0 · SUMPE=NULL|SUMPROP=NULL · SYSTABLES=97 admin /assets/index-CKgq0YlH.js + /assets/index-DD3xQhNt.css eoffice /assets/index-CHMCU35C.js + /assets/index-CdXRNT7E.css (khớp mốc K1 #434) ``` 🔴 **KHAI TRƯỚC — MỤC 4 SẼ RỖNG, KHÔNG PHẢI SẠCH.** Xem mục 4. --- ## Mục 1 — Gitea Actions run ✅ PASS | | | |---|---| | Run | **#435** (id=548), `0779f2d4` | | Status | **success** | | Cửa sổ | created `17:05:33` → updated `17:11:45` = **6m12s** | | URL | https://git.baocaogiaoduc.vn/vietreport-admin/solution-erp/actions/runs/435 | Poll 6 vòng (17:08:31 → 17:12:26), running×5 → success. 6m12s ≈ K1 #434 (6m31s) — hợp lý cho +Mig +11 test, chưa tới ngưỡng escalate. ## Mục 2 — Test gate 609 ✅ PASS (khớp TÁCH-PHẦN, không chỉ khớp tổng) Log web-UI `.../actions/runs/435/jobs/0/logs` (18.677 B), grep nới `Passed![^\r\n]*`: ``` Passed! - Failed: 0, Passed: 45, Skipped: 0, Total: 45, Du Passed! - Failed: 0, Passed: 564, Skipped: 0, Total: 564, Du ``` | | Domain | Infra | Tổng | |---|---|---|---| | K1 #434 | 45 | 553 | 598 | | K2 #435 | **45** | **564** | **609** | | delta | **0** | **+11** | +11 | Kỳ vọng 609 = 598 + 9 lane + 2 gate-fix ⇒ khớp **từng vế**: Domain đứng yên, +11 rơi TRỌN vào Infra — đúng nơi 3 file test mới nằm (`ContractSigningPlanApprovalTests` / `~GroupAndCatalogTests` / `~SchemaTests`). 0 fail, 0 skipped. Control ÂM: grep chuỗi bịa `ZZZ_KHONG_TON_TAI_9x7` = **0 hit**. ⚠️ Dùng regex nới ngay từ đầu vì bài K1: log dùng **2 khoảng trắng** sau `Passed!`. ## Mục 3 — EF migration prod + chứng-restart ✅ PASS ``` MIGTOP=20260731085624_AddKhkkApprovalGroupCatalogAndFinalizeRuntime <- TOP, đúng Mig 71 MIGCOUNT=71 SYSTABLES=97 (GIỮ NGUYÊN — Mig 71 có 0 CreateTable) ``` `sys.tables` 97 == 97 khớp phép đo nguồn `grep -c "CreateTable"` = **0** (control dương: `grep -c "AddColumn"` = **4** ⇒ grep có tìm được thứ có thật). **3-CHÂN-KIỀNG (né bẫy #423 đo-trước-recycle):** 1. *Restart thật:* `appcmd list wp` → `WP "5364" (applicationPool:SolutionErp-Api)`; `Get-Process w3wp` → PID **5364 StartTime 17:11:25** ∈ cửa sổ deploy (17:05:33–17:11:45). (5 w3wp còn lại là pool VIETREPORT/Default — đã map PID→pool để không nhận nhầm.) 2. *Dòng ĐẦU Serilog:* `2026-07-31 17:11:27.310 +07:00 [INF] Applying migrations...` 3. *Mọi phép đo DB dưới đây chạy từ 17:12+* ⇒ SAU mốc recycle. ## Mục 4 — Data prod sau migrate ⚠️ PARTIAL — **CHÂN-LÝ-RỖNG** Số đo nguyên văn (POST-migrate, sau recycle): ``` TOTAL=0 · GROUP1=0 · OUTOFRANGE=0 · LINES=0 · SUMPE=NULL|SUMPROP=NULL CTRL_SUPPLIERS=60 · CTRL_CATALOG=86 <- control dương: bộ đếm CÓ đếm được ``` | Acceptance yêu cầu | Kết quả | Giá trị bằng chứng | |---|---|---| | `COUNT(ApprovalGroup=1) == COUNT(*)` | `0 == 0` ✅ | **RỖNG — 0 bit thông tin** | | `COUNT(ApprovalGroup NOT BETWEEN 1 AND 8) = 0` | `0` ✅ | **RỖNG** | | Σ `PeReferenceAmount`/`ProposedAmount` | `NULL\|NULL` (PRE cũng `NULL\|NULL`) | **RỖNG hai phía** | 🔴 `ContractSigningPlans` prod có **0 dòng cả TRƯỚC lẫn SAU**. Ba acceptance "xanh" nhưng **không mang thông tin nào** về việc backfill có chạy đúng — backfill hụt sẽ trông y hệt. Chính tác giả Mig 71 đã khai bẫy này ("đo local Σ = tập RỖNG hai phía … phép đo prod mới có răng"); nay prod cũng rỗng ⇒ **phép đo prod cũng KHÔNG có răng**. Không hạ thành FAIL (không có gì sai), nhưng CẤM đọc thành PASS. **Bằng chứng THAY THẾ — không phụ thuộc số dòng (CÓ RĂNG):** *(a) 4 cột, đúng kiểu/nullability/default (`INFORMATION_SCHEMA.COLUMNS`):* ``` ContractSigningPlans |ApprovalGroup |int |null=NO |def=((0)) ContractSigningPlans |EndedByLevelFinalize|bit |null=NO |def=(CONVERT([bit],(0))) ContractSigningPlanLines |CatalogEntryId |uniqueidentifier|null=YES|def=- ContractSigningPlanLines |TenHangMuc |nvarchar(500) |null=YES|def=- ``` *(b) HOÁN-ĐỔI INDEX — chứng bằng **liệt kê TRỌN HỌ** (14 index cả 2 bảng), không bằng grep-rỗng:* ``` ✅ CÓ : IX_ContractSigningPlanLines_ContractSigningPlanId_SupplierId_CatalogEntryId unique=1 | filter=([IsDeleted]=(0)) | cols=ContractSigningPlanId,SupplierId,CatalogEntryId ✅ CÓ : IX_ContractSigningPlanLines_CatalogEntryId (cols=CatalogEntryId) ✅ CÓ : IX_ContractSigningPlans_ApprovalGroup_Phase (cols=ApprovalGroup,Phase) ❌ MẤT: IX_ContractSigningPlanLines_ContractSigningPlanId_SupplierId (unique 2-cột cũ) ``` Vắng-mặt của index cũ suy ra từ **danh sách đầy đủ 14 index có thật**, nên đây là vắng-mặt ĐO ĐƯỢC chứ không phải "truy vấn trả rỗng đọc thành sạch". Đây là phép đo mạnh nhất của mục 4. *(c) Bất-biến Σ tiền — chứng theo CẤU TRÚC bằng phép ĐO nguồn:* `grep -nE "PeReferenceAmount|ProposedAmount|ApprovedAmount"` trên Mig 71 → chỉ **2 hit, cả 2 ở dòng 23-24 và đều là comment `//`** ⇒ **0 câu lệnh thực thi** chạm cột tiền. `migrationBuilder.Sql` = đúng **1** câu (dòng 61, backfill `ApprovalGroup=1`). Σ bất biến đúng kể cả khi bảng có dữ liệu. *(d) Materialize qua EF:* xem mục 6 — `?group=N` trả **200** (không 500) ⇒ cột `ApprovalGroup` tồn tại & query được xuyên Domain→Infra→Api trên DB thật. ## Mục 5 — Bundle ×2 app (#77) ✅ PASS — 4/4 ROTATE | app | asset | TRƯỚC (K1 #434) | SAU (K2 #435) | |---|---|---|---| | admin | js | `CKgq0YlH` | **`DVQEwGwf`** | | admin | css | `DD3xQhNt` | **`D050EVYG`** | | eoffice | js | `CHMCU35C` | **`CZAYiWWa`** | | eoffice | css | `CdXRNT7E` | **`DX1FRk5n`** | *Hash CŨ chết (control âm tầng-2 — bundle cũ biến mất khỏi đĩa):* ``` admin /assets/index-CKgq0YlH.js -> 200|text/html|900 (SPA fallback, không phải JS) eoffice/assets/index-CHMCU35C.js -> 200|text/html|876 admin /assets/index-ZZBIADAT.js -> 200|text/html|900 <- hash BỊA, cùng dấu hiệu ``` Hash bịa cho ra ĐÚNG dấu hiệu của hash cũ ⇒ `text/html` thật sự nghĩa là "file không tồn tại" (né bẫy SPA-fallback-200 S72; dùng `Content-Type` chứ không neo byte). *Last-Modified in-window:* admin `10:10:20 GMT` = 17:10:20 +07 · eoffice `10:11:20 GMT` = 17:11:20 +07 — cả hai ∈ (17:05:33, 17:11:45). `Cache-Control: no-cache` cả 2 (hardening S88). ## Mục 6 — Smoke ✅ PASS | Phép đo | Kỳ vọng | Thực tế | |---|---|---| | `/health/ready` | 200 | **200** | | `/health/live` | 200 | **200** | | `/health` trần | (control) | **404** ⇒ 200 ở trên có nghĩa | | `GET /api/contract-signing-plans?group=2` KHÔNG token | 401 | **401** | | route BỊA `/api/contract-signing-plans-KHONG-TON-TAI?group=2` | 404 | **404** | | route BỊA 2 `/api/zzz-fake-9x7` | 404 | **404** | | admin.solutions.com.vn | 200 | **200** | | eoffice.solutions.com.vn | 200 | **200** | 401 (route có thật, có gác) tương phản 404 ×2 (route không có) ⇒ **401 CÓ NGHĨA**. *Có token admin* (login `/api/auth/login`, `accessToken` len=468): ``` ?group=1 -> 200 {"items":[],"total":0,"page":1,"pageSize":20,"totalPages":0,...} ?group=2 -> 200 (thân y hệt) ?group=8 -> 200 (thân y hệt) inbox -> 200 deleted -> 200 ``` "mọi item `approvalGroup==1`" ⇒ **VACUOUS (0 item)** — không tính là bằng chứng. ⚠️ Đã soi **thân RAW** (không tin bộ parse) đúng bài K1 "0-hit là dương-giả tới khi soi format": thân thật là `{"items":[],"total":0}` ⇒ rỗng THẬT, không phải lỗi đọc format. **Chứng `group` ĐƯỢC WIRE (không bị nuốt im lặng):** ``` ?group=abc -> 400 <- bind int THẬT; nếu param không tồn tại, ASP.NET bỏ qua query lạ và trả 200 ``` Khớp nguồn: `ContractSigningPlansController.cs:42` → `[FromQuery(Name = "group")] int? group = null` và `:49` truyền vào `ListContractSigningPlansQuery(..., group)`. **Control DƯƠNG cho đường ống list:** `GET /api/catalogs/contract-catalog` → **86 item** (seed K1 còn nguyên) ⇒ token + serializer + đường ống list CHẠY, nên `items:[]` của KHKK là bảng rỗng thật, không phải ống vỡ. --- ## Lệch / quan sát (không chặn deploy) 1. **`?group=999` → 200** (không 400). Miền nghiệp vụ là 1..8 nhưng biên API không chặn ngoài-miền; hiện vô hại (trả rỗng) và mục 4 đã xác nhận 0 dòng ngoài 1..8. Ghi cho K4b cân nhắc cùng lớp với validator `CatalogEntryId` bắt buộc mà Mig 71 đã hẹn. 2. **Ghost-window `EndedByLevelFinalize`** — tác giả khai trước: K2 đẻ cột, K3 tiêu thụ; tới khi K3 land cột luôn = 0. Đã xác minh cột tồn tại `bit NOT NULL default 0`; **không** coi "toàn 0" là dữ liệu hỏng. 3. **Mục 4 rỗng** — xem trên. Khi có phiếu KHKK thật trên prod, cần đo LẠI 3 acceptance đó thì mới có răng; hiện chưa thể kết luận backfill đúng/sai từ dữ liệu. ## Bảng tổng | # | Mục | Verdict | |---|---|---| | 1 | Gitea run `0779f2d` success | ✅ PASS | | 2 | Test gate 609 = 45D + 564I | ✅ PASS | | 3 | Mig 71 TOP + chứng-restart | ✅ PASS | | 4 | Data prod sau migrate | ⚠️ **PARTIAL — vacuous** (schema/index CÓ răng ✅) | | 5 | Bundle ×2 app rotate + hash cũ chết | ✅ PASS | | 6 | Smoke 401/404/200 + group wired | ✅ PASS | **Không ghi gì lên prod ngoài đọc** (toàn bộ là SELECT / curl GET / login POST lấy token). END — VERDICT=PARTIAL 5/6 (mục 4 vacuous, deploy khỏe, không rollback)