Files
BakNRet/.scratch/ci-cd/cicd_report_20260928.md
Shuery 2cbcaaee48 chore(license): 补 Apache License 2.0
许可证正文**逐字采用** ASF 的 LICENSE-2.0.txt,只把 APPENDIX 里的版权占位行填成
"Copyright 2026 Shuery" —— 正文一个字节都没改。

为此先下载规范文本再打补丁,而不是凭记忆抄一遍:许可证抄错一个词就不再是那个许可证了。
校验方式是逐行对比本仓 LICENSE 与规范文本,结果只差版权那一行(规范文本 11358 字节、
SHA256 cfc7749b…;本仓 11337 字节,差额 21 字节正好等于两行文本的长度差)。

选 Apache-2.0 而非 MIT,主要差别在第 3 节的**明示专利授权**:贡献者授予专利许可,
而不只是版权许可。

同时:
  * README 的许可证章节从「尚未声明」改成实际条款摘要 + 徽章(License: Apache-2.0)
  * BakNRet.psd1 的 PrivateData.PSData 登记 LicenseUri 与 Copyright,Import-Module
    之后可直接读到许可信息
  * 不加 NOTICE:第 4(d) 节只在作品本身带 NOTICE 时才要求向下传递,而本仓库不分发第三方
    代码(Pester / PSScriptAnalyzer 只放在 .tools/ 供本地测试,既进 gitignore 也不进分发产物)
  * .scratch/ci-cd 的两份报告补了带日期的「后续更新」注记,关掉许可证合规的 L-1 与依赖
    安全扫描的 R-1,避免后来的人读到「主许可证未声明」这个已经过期的结论

验收:test.ps1 9/9 全绿(5.1 与 7);静态分析 80 条、0 Error(与改动前持平);
README 相对链接 0 失效、新增外部链接 2/2 返回 200;双宿主解析 psd1 零错、89 个导出不变。
2026-10-02 01:34:45 +08:00

352 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 虚拟机)
> [!NOTE]
>
> **后续更新(2026-10-02)**:本报告里的两条许可证相关待办已关闭 —— 补 `LICENSE`
> (**Apache-2.0**,正文逐字采用 ASF 的 LICENSE-2.0.txt,仅填 APPENDIX 版权行)已落地,
> 对应许可证合规的 **L-1** 与依赖安全扫描的 **R-1**。报告正文保留为**流水线运行当时**的记录。
---
## 阶段总览
| 阶段 | 名称 | 状态 | 关键结果 |
| --- | --- | --- | --- |
| 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 与工作树上完全一致)才定位到是口径差异。
> 报告里凡出现行数处已统一标注口径。
### 建议(按优先级)
| 优先级 | 事项 | 理由 |
| --- | --- | --- |
| ~~高~~ ✅ **已完成 2026-10-02** | 补 `LICENSE` | 已补 **Apache-2.0**(逐字采用 ASF 原文,仅填 APPENDIX 版权行) |
| ~~高~~ ✅ **已完成 2026-10-02** | 提交并同步到云端 | 3 个提交已推送到 `origin/main`(快进,非 force) |
| 中 | 把 `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 的报告生成方独立渲染并留证 |