# BakNRet CI/CD 流水线报告 - **报告时间**:2026-09-28 - **项目**:BakNRet(Windows 备份 / 恢复工具,PowerShell 5.1 + 7.x) - **分支**:`refactor/ms-conventions` - **测试环境**:Hyper-V VM `BakNRet-Lab`(Gen2 / 8 vCPU / 12 GB)+ 宿主 `STRIX-X870A` - **流水线入口**:`/ci-cd-pipeline`(用户指定:测试环境为 Hyper-V 虚拟机) --- ## 阶段总览 | 阶段 | 名称 | 状态 | 关键结果 | | --- | --- | --- | --- | | 0.1 | 依赖安全扫描 | ✅ 通过 | 零运行时依赖;**0 致命/严重**;2 条一般级卫生风险 | | 0.2 | 许可证合规 | ⚠️ 通过(有缺口) | 无 copyleft 冲突;**主许可证未声明** | | 0.3 | 语法检查与修复 | ✅ 通过 | 131/131 双宿主解析零错;修 2 处排版 + 1 处自伤;分析器 84→80 条、0 Error | | 1 | 黑盒测试 | ✅ 通过 | 11 条用例:9 通过、2 失败(**0 致命/严重**) | | 2 | 缺陷修复与回归 | ✅ 通过 | 修 1 个真实缺陷;回归 **12/12**;Pester 183→**192** 全绿 | | 3 | 测试增强与性能 | ✅ 通过 | 性能基线首次建立、**无退化**;单测增强给出优先级、未擅自新增 | | 4 | 代码规范化 | ✅ 通过 | 分析器格式规则门禁通过(0 Error);仓库自有格式门禁为准 | | 5 | 文档生成与质量检查 | ✅ 通过 | `PROJECT_REPORT.md` 532 行;markdownlint **0 错**;死链 **0** | | 6 | 数据库迁移审查 | ⏭️ **不适用** | 全仓无数据库、无 SQL、无迁移脚本 | | 7 | 生产部署 | ⛔ **停在人工确认门** | 需你确认(见文末) | | 8 | 收尾 | ✅ 完成 | 本报告 | --- ## 阶段 0:静态分析与修复 ### 0.1 依赖安全扫描 | 项目 | 结论 | | --- | --- | | 依赖管理文件 | 不存在(无 `package.json` / `requirements.txt` / `go.mod` / `pom.xml` / `Cargo.toml`) | | 运行时依赖 | **0 个模块**(只需 PowerShell 5.1 或 7.x + 7-Zip) | | CVE | 无可报(没有任何被本仓库固定版本的第三方库) | **风险**:R-1 无 `LICENSE`(一般)· R-2 工作区存在口令文件 `baknret.key`(一般,**未被 git 跟踪**, `.gitignore:18 *.key` 已忽略;全程未读取其内容)· R-3 不检查 7z 版本(建议)· R-4 口令经命令行传给 7z(建议,上游限制)。 ### 0.2 许可证合规 | 组件 | 许可证 | 是否随分发 | | --- | --- | --- | | 本项目 | **未声明** | — | | Pester 5.9.1 | Apache-2.0 | 否(`.tools/`,gitignore) | | PSScriptAnalyzer 1.25.0 | MIT | 否 | | Newtonsoft.Json | MIT | 否 | | 7-Zip 26.03 | LGPL-2.1 + BSD-3 + unRAR 限制 | 否(用户自备) | **无 copyleft 传染**。7-Zip 的 unRAR 限制只禁止「用其代码还原 RAR 压缩算法」,本项目不触发。 ### 0.3 语法检查与自动修复 **修复的真实缺陷** | 文件:行 | 规则 | 修复 | | --- | --- | --- | | `Backup.ps1:42` | `PSUseConsistentWhitespace` | `@($Rest)+ @(...)` → `@($Rest) + @(...)` | | `Restore.ps1:42` | 同上 | 同上 | **修复过程中的自伤与恢复(重要教训)** 编辑这两个文件后 **UTF-8 BOM 被抹掉**,导致 5.1 立即报 `The string is missing the terminator: '.`, 而 PowerShell 7 完全正常。用 `UTF8Encoding($true)` 重写后恢复,双宿主 `parse_bad=0`。 这条恰好实证了本仓库 Encode 层门禁的价值,也说明「编辑 PowerShell 源文件必须保留 BOM」是硬约束。 **未修复项**:80 条全部为风格类(0 Error),按用户决定登记为「仓库已声明的偏离」,逐条附依据: 行长 160 是配置里写明的有意偏离(`PSScriptAnalyzerSettings.psd1` + [ADR-0008](../docs/adr/0008-analyzer-deviations.md)); `PSPlaceCloseBrace` 等集中在 `Run-Tests.ps1` 的**测试夹具字符串**内(改了会改变断言语义); `PSUseSupportsShouldProcess` 已全局排除属已知噪声;空 catch 是有意为之(注释已写明理由)。 **待人工确认的观察项**:`Write-BakNRetRunSummary` 的 `$Mode` 参数声明了 `ValidateSet` 与 `Mandatory` 但函数体内从未读取,且该函数未被仓库内任何代码调用(仅对外导出)。判断为未完工实现, 删除会破坏公共 API 参数契约,故**登记而不改动**。 --- ## 阶段 1 → 2:黑盒测试与缺陷修复 ### 测试环境确认(技能强制) | 项目 | 事实 | | --- | --- | | 目标 | Hyper-V VM `BakNRet-Lab`(Gen2 / 8 vCPU / 12 GB / Default Switch 内部 NAT) | | 检查点 | 自动检查点、**clean-baseline**、**sandbox-ready** | | 数据隔离 | 仓库自述:全部操作在 VM 内进行,宿主机仓库 / `Backups\` / `logs\` 不被写入 | | 生产暴露 | 无(无生产服务器、无域名、无对外服务) | **已获用户明确确认**后开始测试。 ### 用例与结果 | 编号 | 用例 | 阶段 1 | 阶段 2 回归 | | --- | --- | --- | --- | | BB-01 | 真实备份:退出码 0 + 产出归档 + manifest | PASS | PASS | | BB-02 | 排除模式真的把内容挡在归档之外(逐个归档内容核对) | PASS | PASS | | BB-03 | `-DryRun` 一个字节都不写(manifest SHA256 前后一致) | PASS | PASS | | BB-04 | 源不存在记 `missing-source`,不算失败 | PASS | PASS | | BB-05 | `-Only` 只处理匹配的条目 | PASS | PASS | | BB-06 | **显式清单不存在 → 必须报失败** | **FAIL** | **PASS(已修)** | | BB-06b | 对照:未显式指定时首次运行仍建模板 | — | PASS | | BB-07 | 旧名字垫片转发且退出码原样传递 | PASS | PASS | | BB-08 | 5.1 与 7.x 行为一致 | PASS | PASS | | BB-09 | 含 `&` 与 `#` 的路径(整行引号写法) | PASS | PASS | | BB-10 | 无控制台且无 `-InputScript` 时报错退出,绝不挂起 | PASS | PASS | | BB-11 | 孤儿归档审计 | FAIL(夹具错误) | PASS | - **阶段 1**:11 条 → 通过 9、失败 2、**致命/严重 0** - **阶段 2 回归**:12 条 → **全部通过** ### 缺陷清单 | ID | 严重程度 | 标题 | 根因 | 修复 | | --- | --- | --- | --- | --- | | **DEF-01** | 一般 | 显式指定的清单不存在时**静默成功** | 「首次运行引导」与「路径写错」两条语义共用一条代码路径 | 按 `$PSBoundParameters.ContainsKey('BackupListPath')` 分流:显式指定 → ERROR + `exit 1`;未指定 → 保留引导 | | DEF-02 | 提示 | 两处运算符前后缺空格 | 手写拼接 | 补空格(分析器该规则归零) | | DEF-03 | 提示 | 孤儿审计在干跑下不报告 | **测试夹具错误**(非产品缺陷) | 修正夹具为真实运行 | **DEF-01 的影响**:计划任务里 `-BackupListPath` 写错(或相对路径按了别的工作目录解析)时, 脚本会在错误位置凭空建一份模板并 **exit 0**;任务计划程序读到「上次运行结果 = 成功」, 而实际一个条目都没处理。这与本仓库已修过的「27 条被静默跳过、退出码仍是 0」是同一类缺陷, 因此判为需要修复。`Restore-Data.ps1` 有对称问题(生成清单 + exit 0),一并修复。 **DEF-03 的处置值得记录**:BB-11 初判 FAIL,复核后确认是**用例设计错了**(用了 `-DryRun`, 而孤儿审计只在真实运行里报告)。保留该记录,因为它说明「失败用例必须先复核再归因」, 否则会把测试自身的问题记到产品头上。 ### 回归证据 ```text BakNRet 验收:层次 [Parse, Unit, Smoke, E2E],宿主 [7, 5.1] [PASS] Encode any 受管 133 个文件:缺 BOM 0、CRLF 0、制表符 0 [PASS] Parse 7 解析通过 131/131 个文件 [PASS] Unit 7 / Smoke 7 / E2E 7 [PASS] Parse 5.1 解析通过 131/131 个文件 [PASS] Unit 5.1 / Smoke 5.1 / E2E 5.1 验收全部通过:9 项(宿主 7 + 5.1) ``` ```text Tests Passed: 192, Failed: 0, Skipped: 0, Inconclusive: 0, NotRun: 0 [+] [回归] 显式指定的清单不存在时报失败(退出码 1),且不在错误位置建模板 1.43s [+] [回归] 未显式指定清单时的首次运行仍建模板并退出 0(引导不能被误伤) 1.44s ``` 新增的 2 条用例是**成对**的:只测一条的话,「把所有缺失都改成 exit 1」这种错误实现也能骗过测试。 --- ## 阶段 3:测试增强与性能验证 ### 单元测试增强(可选步骤) | 指标 | 数值 | | --- | --- | | 对外函数 | 89(与 `FunctionsToExport` 白名单逐一核对一致) | | 测试中直接点名 | 70(78.7%) | | 未直接点名 | 19(其中多条由集成路径间接覆盖) | **建议补测**(本轮未擅自新增,因用户选择了最小侵入姿态,且该步骤标注为可选): `Move-BakNRetArchiveIntoPlace`(原子替换,历史上真出过「先删后移」缺陷)、 `Resolve-BakNRetRootedPath`(相对路径按仓库根解析这条计划任务关键约定)。 安全描述符相关的 4 个函数**不建议**补单元测试 —— 它们需要真实提权与真实 NTFS, 仓库已用 `tools/lab/Lab.ps1 acl-test`(含负对照)在真 VM 里覆盖,那是更正确的层次。 ### 性能基线(首次建立) 环境:宿主 `STRIX-X870A`,PowerShell 7.7.0-preview.5,7-Zip 26.03 (x64) | 指标 | 采样 | min | 中位数 | max | | --- | --- | --- | --- | --- | | 模块导入 | 5 | 628.3 ms | **631.0 ms** | 2052.8 ms | | 清单解析 ×50 轮 | 5 | 2535.9 ms | **2546.9 ms** | 2691.5 ms | | 只读干跑(真实 28 条清单) | 5 | 9839.9 ms | **9869.2 ms** | 12900.8 ms | | 源树扫描(200 文件 ×3 轮) | 1 | — | **18.2 ms** | — | 7z 吞吐(固定夹具 200 × 32 KB **随机数据**):`-mx=0` 46.7 ms / `-mx=5` 224.2 ms / `-mx=9` 225.2 ms。 夹具为不可压缩数据,故体积不随级别变化 —— 该夹具测的是**吞吐与 I/O,不是压缩率**。 **判定**:首次基线,无历史可比,**无退化**。基线文件:`.scratch/ci-cd/20260928-001722/perf_baseline.json`。 跨机比较无意义,仅同机同宿主下对比。 --- ## 阶段 4:代码规范化 - **仓库自有的格式门禁**是 `PSScriptAnalyzerSettings.psd1` 打开的 6 条格式规则 (括号 / 缩进 / 空格 / 对齐 / 大小写)+ 160 字符行长,通过 `tools/Invoke-Analyzer.ps1` 执行。 - 本次:**0 条 Error**,格式类告警从 4 条(whitespace)降到 **0 条**。 - PowerShell 没有官方「Google 风格」格式化器;本仓库的等价物就是上面这套规则集, 故阶段 4 以仓库门禁为准,未引入外部格式化器(避免制造与仓库约定冲突的大 diff)。 - `README.md` / `PROJECT_REPORT.md` 由 **Prettier 3.9.9** 格式化并通过 `--check`。 --- ## 阶段 5:文档生成与质量检查 | 产物 | 规模 | 质量门禁 | | --- | --- | --- | | `README.md` | 925 行 | Prettier ✅;markdownlint 17 → **10 错(全为 MD051 假阳性)**;外部链接 11/11 → 200;内部相对链接全通;63 个标题锚点全部可解析;3 张 Mermaid 图在真实浏览器中解析通过 | | `PROJECT_REPORT.md` | 532 行 | Prettier ✅;markdownlint **0 错**;相对链接 18/18 有效;外部链接 3/3 → 200 | **MD051 假阳性说明**:README 的标题带 emoji(如 `## 🚀 快速开始`),GitHub 生成的锚点是 `#-快速开始`(剥离 emoji 后前导连字符)。`markdownlint-cli2` 的锚点生成器**不做 emoji 剥离**, 因此把这类链接报为无效。已用独立脚本按 GitHub 规则复算全部锚点,**63 个全部可解析**, 故不修改标题(emoji 标题是本次文档改造的要求之一)。 **死链检查**:全部外部链接逐个 `HEAD` 请求验证,返回 200;相对链接逐个 `Test-Path` 验证存在。 --- ## 阶段 6:数据库迁移审查 —— 不适用 | 检查 | 结果 | | --- | --- | | `*.sql` / `*.db` / `*.sqlite` 文件 | 无 | | `Invoke-Sqlcmd` / `SqlConnection` / `CREATE TABLE` / `SELECT ... FROM` | 无命中 | | 迁移脚本目录 | 不存在 | 本项目是本地 CLI 工具,**不涉及任何数据库**,该阶段跳过。 --- ## 阶段 7:生产部署 —— 停在人工确认门 > [!CAUTION] > > **未执行任何部署动作。** 本项目的「生产部署」= 在真实机器上注册计划任务并让它按计划跑备份, > 属于对用户个人环境产生持久副作用的操作,必须由你确认。 ### 部署前状态 | 项目 | 状态 | | --- | --- | | 代码门禁 | ✅ 9/9(双宿主)· Pester 192/192 · 黑盒 12/12 | | 工作树 | 6 个文件已改(+796 / −202),新增 `PROJECT_REPORT.md`(未提交) | | 运行前提 | PowerShell 5.1/7.x ✅ · 7-Zip 26.03 ✅ | | 阻塞项 | 会话审批策略为 `never`,`gsudo` 提权被自动拒绝(退出码 999);注册计划任务需要管理员 | ### 建议的部署步骤(待你确认后执行) ```powershell # 1. 先确认清单与口令就位(口令应在仓库外) .\Backup-Data.ps1 -DryRun # 只读,先看计划与空间预估 .\Backup-Data.ps1 -Only 'Edge' # 先只备一项,验证端到端 .\Restore-Data.ps1 -VerifyOnly # 只校验,不解压 # 2. 注册计划任务(需要管理员) .\tools\Register-BackupTask.ps1 -At '21:30' -DryRun # 先看将要注册什么 .\tools\Register-BackupTask.ps1 -At '21:30' # 确认后注册 # 3. 部署后验证 Get-ScheduledTaskInfo -TaskName 'BakNRet Backup' # 上次运行结果 Start-ScheduledTask -TaskName 'BakNRet Backup' # 手动跑一次 ``` ### 回滚 ```powershell .\tools\Register-BackupTask.ps1 -Remove ``` **风险**:注册计划任务本身可逆(`-Remove`);但**首次真实备份会写入归档目录并消耗磁盘** (README 已有空间预估与逐条目守卫,不够时只告警不中断)。 --- ## 阶段 8:关键指标与建议 ### 关键指标 | 指标 | 数值 | | --- | --- | | 验收门禁 | **9/9 PASS**(PS 7.7.0-preview.5 + 5.1.26100.9502 各一遍) | | 解析 | **131/131** 文件双宿主零错 | | Pester | **192 通过 / 0 失败 / 0 跳过**(183 → 补 2 条回归用例后 185 → 再补 7 条后 192) | | 黑盒回归 | **12/12** | | 静态分析 | **80 条 / 0 Error**(修复前 84) | | 运行时依赖 | **0** | | 致命 / 严重缺陷 | **0** | | 修复的真实缺陷 | **1**(DEF-01)+ 2 处排版 | | 代码变更 | 6 文件,+796 / −202 | | 文档产物 | `README.md` 925 行 · `PROJECT_REPORT.md` 1004 行 | ### 后续轮次已执行(用户回复「继续」之后) | 事项 | 结果 | | --- | --- | | 补单元测试(原建议 2 条) | ✅ 实际补 **7 条**断言:原子替换 3 条(含 AST 判定 `File.Replace` 与 `[NullString]::Value`)、路径解析 4 条(含 AST 判定「模块里不得引用 `$PSScriptRoot`」)。Pester 185 → **192**,0 失败 | | 修正 README 的零依赖套件条数 | ✅ 实测为 **128 项**(原写 111,已改正)。这是上一轮 README 重写留下的**事实错误**,由 PROJECT_REPORT 的核对暴露出来 | | 复核 PROJECT_REPORT.md 的数字 | ✅ 行数口径标注为「**非空行**」;Pester 计数更新为 192;**修正一处过度断言**(原文称「修复后代码在 VM 内验证可用」,实际只取到修复前的 Pester 快照) | | 补 LICENSE / 移出 `baknret.key` / 确认 `$Mode` 意图 | ⏸️ 仍需你决定 | > **指标口径说明(本次踩到的坑)**:工程规模按**非空行**统计是 **14389 行**,按**含空行**统计是 **16805 行**。 > 两者都对,但必须写明口径。本次就因为口径不同(我用含空行、报告用非空行),一度把一份**正确的**数字 > 误判为错误,直到逐目录复算(模块 4369 非空行在 HEAD 与工作树上完全一致)才定位到是口径差异。 > 报告里凡出现行数处已统一标注口径。 ### 建议(按优先级) | 优先级 | 事项 | 理由 | | --- | --- | --- | | **高** | 补 `LICENSE` | 当前等于「保留所有权利」,他人无权分发 | | **高** | 提交前复核本次改动并决定是否 commit | 6 个文件已改,尚未提交 | | 中 | 把 `baknret.key` 移出仓库目录 | 虽未被跟踪,工作区放口令文件是卫生问题 | | 中 | 确认 `Write-BakNRetRunSummary` 的 `$Mode` 意图 | 未使用参数 + 未被调用 | | 低 | 在 VM 内补跑修复后的三套件 | 本次因提权被禁未取得(宿主侧已全绿) | | 低 | 启动时打印 7-Zip 版本 | 便于排障 | ### 本次流水线的诚实边界 1. **VM 测试未跑全**:会话审批策略改为 `never` 后 `gsudo` 提权被自动拒绝,而 Hyper-V 与 PowerShell Direct 都需要管理员。**已核实的部分**(同步成功、VM 内 Pester 183 通过)如实记录; 零依赖与端到端套件在 VM 内的结果**本次未取得**,不做推断。宿主侧同等套件已全部通过。 2. **静态分析未压到 0**:剩余 80 条与仓库**已声明的偏离**冲突,按你的决定登记而不改, 逐条附依据。严格模式的「零容忍」在这里与仓库自身配置相抵,选择尊重仓库约定。 3. **阶段 7 未执行**:等你在下一轮明确确认。 --- ## 附件索引 所有中间产物在 `.scratch/ci-cd/`: | 文件 | 内容 | | --- | --- | | `20260928-001722/dependency_security_report.md` | 依赖安全扫描报告 | | `20260928-001722/license_check_report.md` | 许可证合规报告 | | `20260928-001722/stage0_static_analysis.md` | 阶段 0 完整报告(含 BOM 自伤记录) | | `20260928-001722/stage3_tests_and_perf.md` | 阶段 3 报告(覆盖率分析 + 性能基线) | | `20260928-001722/analyzer_raw.txt` | 修复前静态分析原始输出(84 条) | | `20260928-001722/analyzer_after.txt` | 修复后静态分析输出(80 条) | | `20260928-001722/pester_after_fix.txt` | 修复后 Pester 详细输出(185 通过;本轮补测后 192) | | `20260928-001722/perf_baseline.json` | 性能基线数据 | | `20260928-001722/blackbox_results.json` | 阶段 1 黑盒结果(11 条) | | `20260928-001722/blackbox_regression.json` | 阶段 2 回归结果(12 条) | | `blackbox-tests.ps1` | 黑盒用例脚本(可重跑) | | `blackbox-regression.ps1` | 回归脚本(可重跑) | | `perf-baseline.ps1` | 性能基线脚本(可重跑) | | `../_verify/mermaid.png` | README 三张 Mermaid 图的浏览器渲染截图 | | `../_verify/report_mermaid.png` | PROJECT_REPORT 四张图的渲染截图(本次独立复验;与 `20260928-001722/report_mermaid.png` 是**两次独立渲染**,字节不同属正常) | | `20260928-001722/report_mermaid.png` | 同上四张图,由阶段 5 的报告生成方独立渲染并留证 |