经典项目的合并流程是一场信任游戏:提交信息写着”已自测”,然后一个被本机脏缓存喂绿的构建进入主干,晚上把所有人的流水线染红。BitzOrcas 不接受这种赌博——质量门禁在本地与 CI 中运行同一套脚本,因此本地合并通过即可预测 CI 构建通过。
唯一的合并入口是针对显式 BitzOrcas.Modern.slnx 运行的 scripts/build/verify-all.sh。对于阶段完成或 PR 报告,scripts/build/report-acceptance.sh 以关闭 fail-fast 的方式运行同样步骤,并生成 .verify-output/acceptance-report.md 作为评审材料。
每次合并的门禁
十条命令无需 Docker 即可运行;一条 Docker 条件门禁覆盖 Testcontainers 集成测试。
| # | 命令 | 门禁 |
|---|---|---|
| 1 | dotnet restore BitzOrcas.Modern.slnx | Central Package Management 还原 |
| 2 | dotnet format BitzOrcas.Modern.slnx --verify-no-changes | .editorconfig 格式一致性 |
| 3 | dotnet build BitzOrcas.Modern.slnx -c Release | 0 错误、0 警告(TreatWarningsAsErrors 全局开启) |
| 4 | dotnet test BitzOrcas.Modern.slnx --filter "FullyQualifiedName!~Integration" | 单元 + 应用测试 |
| 5 | check-xml-files.sh | XML 良构性(csproj/props/targets/slnx/config/resx) |
| 6 | check-xml-comments.sh | XML 文档规则(公开声明完整标注、无裸 inheritdoc) |
| 7 | dotnet test BitzOrcas.Architecture.Tests | 依赖方向、模块边界、禁止引用 |
| 8 | dotnet test BitzOrcas.Integration.Tests --filter "Category!=Docker" | DI 闭包冒烟 |
| 9 | dotnet publish src/Hosts/BitzOrcas.Api -c Release -p:PublishTrimmed=true | 硬门禁 裁剪发布与 AOT 兼容性 |
| 10 | dotnet test BitzOrcas.Framework.ConsumerContract.Tests | 目录驱动的本地 Feed、空缓存还原/构建/测试/发布 |
dotnet test tests/BitzOrcas.Integration.Tests --filter "Category=Docker"(Testcontainers / MsSqlBuilder / RabbitMqBuilder)是 Docker 条件门禁;任何使用这些 builder 的类都必须带 Docker 特征,否则会在无 Docker 的开发机上随机爆炸。
CI 矩阵
CI 定义在 .github/workflows/ci.yml,触发条件是 main 分支的 push/PR 加每日定时巡检;解决方案固定为 BitzOrcas.Modern.slnx,所有命令带 --disable-build-servers 保证可复现。五个任务各管一段风险:
| 任务 | 职责 |
|---|---|
portability-gate | ubuntu/windows/macos 三 OS 矩阵还原 + Release 构建,随后跑架构门禁测试(前置 test-architecture-process-profiles.sh all),防止平台私有 API 渗入框架 |
fast-gate | Gitleaks 容器密钥扫描、dotnet format --verify-no-changes、IDE0005 未用 using 清零、OpenAPI 漂移检查(check-openapi-drift.sh)、分层测试过滤、XML 文件与 XML 注释检查 |
release-candidate | 商业候选产物的组装前置校验 |
publish-trim | linux-x64 / win-x64 / osx-arm64 三 RID 的 -p:PublishTrimmed=true 发布矩阵——任何被裁剪击穿的反射调用在此现形 |
integration-docker | Testcontainers 拉起真实 SQL Server / Redis / RabbitMQ 的契约测试 |
商业 GA 是独立保护工作流(commercial-ga.yml 等):对签名产物做只读验证,独立的 build-and-scan 与 sign-finalize-attest 作业钉到完整提交 SHA,签名环境中不执行任何仓库脚本。见商业 GA 门禁。
测试分层
仓库刻意没有引入数字型覆盖率门槛——coverlet 等采集器不在依赖账本中,CI 也没有覆盖率步骤。质量语义由脚本化硬门禁承载:各层级定向测试必须全绿,核心路径必须包含失败分支与负向用例。评估一次改动的验证充分性时,回答”这个风险被哪条门禁断言”,而不是报一个百分比。
| 层 | 内容 | 工具 |
|---|---|---|
| Unit | 纯领域/规则 | xUnit + Shouldly + NSubstitute |
| Application | Handler/Pipeline/Authorization/Result/Port 契约 | xUnit + Shouldly + NSubstitute |
| Integration | DB / CAP / RabbitMQ / API / Auth / Outbox | Testcontainers(镜像按 digest 钉死) |
| Architecture | 依赖方向、模块边界、禁止引用 | ArchUnitNET |
核心路径(事务/outbox/认证/数据权限)必须包含失败路径与负向测试。InMemoryDb 不能替代核心集成测试——它测不出真实的 SQL 方言差异、连接池耗尽和事务隔离行为。
数据/事务一致性矩阵
八种场景必须自动化:成功的 Command 提交业务数据;成功的 Command 写入 outbox;Result.Failure 回滚;系统异常回滚;RabbitMQ 不可用时 outbox 仍可恢复;租户 QueryFilter 生效;软删除 QueryFilter 生效;审计异步写入不阻塞业务事务。这八个场景是水平切片,任何一个新模块接入持久化后都要在同一套语义下复验。
API 状态码契约
每个错误都是一个符合 RFC 9457 的 application/problem+json:标准字段 type/title/status/detail/instance,外加可观测性扩展 errorCode/errorType/traceId/correlationId/requestId。映射入口集中在 Results/ResultExtensions.ToProblem,端点不允许自行分叉拼装错误体。
| 状态码 | 含义 |
|---|---|
| 400 | 请求结构/格式/基础校验 |
| 401 | 未认证 |
| 403 | 无权限或套餐不允许 |
| 404 | 资源不存在 |
| 409 | 并发/幂等/状态冲突 |
| 422 | 业务规则未满足 |
| 429 | 触发限流 |
| 500 | 仅系统异常 |
真实的 409 响应形状如下(errorCode 取自强类型错误目录中的既有条目,前端按 code 精确分流,title/detail 走外部 i18n 本地化):
{ "type": "https://docs.vnext.ailinkedlaw.com/errors/AI.Message.RequestConflict", "title": "Request conflict", "status": 409, "detail": "同一会话已有进行中的请求,请等待其完成或先取消。", "instance": "/api/ai/conversations/3fa1c2/messages", "errorCode": "AI.Message.RequestConflict", "errorType": "Conflict", "traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01", "correlationId": "6f3a2c9e4d7b4f1a", "requestId": "0HN7GJQKQV9PF0001"}errorCode 遵循 {Owner}.{Scenario}.{Reason} 三段形态,全部条目登记在 0008-error-catalog.json(当前 1,968 条);编造目录之外的 code 属于门禁违规。
REQ-GATE-001——反射零容忍
MakeGenericType(、GetMethod(、Activator.CreateInstance(、Assembly.GetTypes( 在运行时代码中是 P0 阻断;Assembly.GetType( 是 P1 警告。SqlSugar 适配器/Application/Domain 中任何未登记的 IL 事件自动升级为 P0 并阻断合并。扫描器是 scripts/build/check-reflection.sh,底层驱动 src/Tooling/BitzOrcas.ReflectionGuard.Cli;配套突变测试保证扫描器本身不腐化。
确有必要的反射逃生口需要三件套:// AOT-EXEMPT: 注解说明业务理由、覆盖裁剪场景的单元测试、以及登记到 docs/architecture/06-aot/0601-aot-exception-ledger.md 例外台账:
// 未经豁免的 Assembly.GetType 是 P1 警告;补充以下三行使其可通过门禁。// AOT-EXEMPT: 仅启动期诊断探针使用,目标类型以 [DynamicallyAccessedMembers] 显式保留。var probeType = assembly.GetType(probeTypeName);// 台账条目还需注明 owner、删除条件与复审日期,季度复核未满足即删除豁免。见 ADR 0103 了解源生成替代反射的整体策略。
REQ-GATE-002——禁止 T-SQL
check-no-tsql.sh 扫描 .cs 字符串字面量并硬阻断 "SELECT "、"INSERT "、"UPDATE "、"DELETE "、"EXEC "(除非在白名单内则为 P0)。白名单覆盖审计 Dapper 读侧例外与 Dapper Query Store 只读查询,每季度复核——每一次复核都必须回答同一个问题:这条 SQL 为什么还不能换成 ORM 表达式?替代方案:ORM Lambda/LINQ、Mapperly ProjectToDto()、[BitzTable] 驱动的 ORM 配置。
REQ-GATE-003——ArchUnitNET 断言与禁用包
架构测试工程 BitzOrcas.Architecture.Tests(TngTech.ArchUnitNET + Shouldly + xUnit,约百个测试类)持续断言:Domain 无上游依赖;Application 无 Infrastructure 依赖;Domain/Application 不引用任何 ORM 包;跨模块调用只能指向 {Module}.Contracts;Mapperly 仅出现在 Infrastructure/Workflow;IRepository<T> 签名不含 ORM 类型;任何地方都不引用 FluentValidation。
技术选型红线同样机器化。以下是 BannedPackagesTests 的核心逻辑摘录(注释为文档补充),这份前缀清单就是 ADR 层面”选型矩阵”的可执行形态:
// 违禁前缀来自 ADR 0101/0002/0003 决议;调整此清单必须先修订对应 ADR。private static readonly string[] BannedPrefixes =[ "Autofac", // 容器统一内置 Microsoft.Extensions.DependencyInjection "Newtonsoft.Json", // 序列化统一 System.Text.Json "Swashbuckle", // OpenAPI 文档由内置管线与源生成产出 "AutoMapper", // 对象映射统一 Mapperly 编译期生成 "FluentValidation", // 校验统一 IRequestRule 纯函数规则管道 "Hangfire", "Finbuckle", "MediatR", // CQRS 消息面统一 Mediator 源生成方案 "Mapster",];
[Fact]public void Project_Assemblies_Should_Not_Reference_Banned_Libraries(){ // 1. 枚举每个受控程序集的直接引用,比对违禁前缀。 var offenders = ( from assembly in ProjectAssemblies from reference in assembly.GetReferencedAssemblies() let name = reference.Name ?? string.Empty from banned in BannedPrefixes where name.StartsWith(banned, StringComparison.Ordinal) select $"{assembly.GetName().Name} -> {name}").ToList();
// 2. 一条违规即可让 fast-gate 变红,并在断言消息里给出完整的依赖链便于定位。 offenders.ShouldBeEmpty($"禁止包被项目程序集引用:\n{string.Join("\n", offenders)}");}同类红线还包括:日志抽象隔离(Domain/Application 不得出现 Serilog 依赖)、时间读取一律经由 IAppClock(全域扫描禁止直读 DateTime.Now/UtcNow,保证利冲窗口、时效计算等场景可被确定性测试)。完整清单见测试工程的 CodingRedLineTests。
模板完成定义
十个条件定义一个完整的架构模板:文档含 ADR/context-map/selection-matrix/redlines;代码含 4 层项目 + AppHost + ServiceDefaults + 测试;一条真实业务切片跑通 API→DB→outbox;架构测试阻断错误依赖;集成测试拉起真实 SQL Server/Redis/RabbitMQ;CI 运行 build/unit/integration/architecture/trim;模板只生成 shell + 业务源码(绝不复制 Framework/Platform/Licensing 核心);每个 Profile 完成空缓存还原/构建/测试/发布;商业包通过签名/hash/SBOM/漏洞/许可证门禁;Package Entitlement、Runtime License、租户 Feature Entitlement 分层并含在线/离线/失败状态测试。
少一条,交付的就不是一个架构模板,而是一份需要收件人补课的源码压缩包。