Skip to content
bitzorcas
中EN

Concept

Quality gates

The verify-all flow, CI matrix, test layers, API status-code contract, data-consistency test matrix, AOT/reflection/T-SQL gates, and the template completion definition that gate a BitzOrcas merge.

Last updated

In most projects a merge is an act of trust: the commit message says “tested locally,” then a build fed green by a dirty machine cache lands on main and turns everyone’s pipeline red that evening. BitzOrcas refuses that gamble — the quality gates run the same script set locally and in CI, so a passing local merge predicts a passing CI build.

The single merge entry point is scripts/build/verify-all.sh against the explicit BitzOrcas.Modern.slnx. For a stage-completion or PR report, scripts/build/report-acceptance.sh runs the same steps with fail-fast off and emits .verify-output/acceptance-report.md for review.

failurefailurefailure

Source & manifests

Fast build / unit / architecture gates

Integration & Docker contracts

Trim, packaging, templates & supply chain

Reviewable release evidence

Block merge

Per-merge gates

Ten commands run without Docker; one Docker-conditional gate covers Testcontainers integration tests.

#CommandGate
1dotnet restore BitzOrcas.Modern.slnxCentral Package Management restore
2dotnet format BitzOrcas.Modern.slnx --verify-no-changes.editorconfig formatting consistency
3dotnet build BitzOrcas.Modern.slnx -c Release0 errors, 0 warnings (TreatWarningsAsErrors)
4dotnet test BitzOrcas.Modern.slnx --filter "FullyQualifiedName!~Integration"Unit + application tests
5check-xml-files.shXML well-formedness (csproj/props/targets/slnx/config/resx)
6check-xml-comments.shXML doc-comment rules (full coverage, no bare inheritdoc)
7dotnet test BitzOrcas.Architecture.TestsDependency direction, module boundaries, banned references
8dotnet test BitzOrcas.Integration.Tests --filter "Category!=Docker"DI-closure smoke
9dotnet publish src/Hosts/BitzOrcas.Api -c Release -p:PublishTrimmed=trueHard gate trim publish & AOT compatibility
10dotnet test BitzOrcas.Framework.ConsumerContract.TestsCatalog-driven local feed with cold-cache restore/build/test/publish

dotnet test tests/BitzOrcas.Integration.Tests --filter "Category=Docker" (Testcontainers / MsSqlBuilder / RabbitMqBuilder) is the Docker-conditional gate; every class using those builders must carry the Docker trait, otherwise it detonates randomly on Docker-less developer machines.

CI matrix

CI is defined in .github/workflows/ci.yml, triggered by push/PR to main plus a daily cron sweep. The solution is pinned to BitzOrcas.Modern.slnx and every command passes --disable-build-servers for reproducibility. Five jobs split the risk:

JobResponsibility
portability-gateubuntu/windows/macos matrix restore + Release build, followed by architecture gate tests (preceded by test-architecture-process-profiles.sh all) — keeps platform-private APIs out of the framework
fast-gateGitleaks container secret scan, dotnet format --verify-no-changes, IDE0005 unused-usings zeroed, OpenAPI drift check (check-openapi-drift.sh), layered test filters, XML file and XML comment checks
release-candidatePre-assembly validation of commercial release candidates
publish-trim-p:PublishTrimmed=true publish across linux-x64 / win-x64 / osx-arm64 — this is where reflection calls broken by trimming surface
integration-dockerTestcontainers contract tests against real SQL Server / Redis / RabbitMQ

Commercial GA runs in separate protected workflows (commercial-ga.yml etc.): read-only verification of signed artifacts, with dedicated build-and-scan and sign-finalize-attest jobs pinned to full commit SHAs — no repository scripts execute inside the signing environment. See Commercial GA gate.

Test layers

The repository deliberately ships no numeric coverage gate — coverlet-style collectors are absent from the package ledger and CI runs no coverage step. Quality semantics ride on scripted hard gates: every layer’s targeted tests must be green, and core paths must include failure branches and negative cases. When judging whether a change is adequately tested, answer “which gate asserts this risk,” never a percentage.

LayerScopeTooling
UnitPure domain/rulesxUnit + Shouldly + NSubstitute
ApplicationHandler/Pipeline/Authorization/Result/Port contractsxUnit + Shouldly + NSubstitute
IntegrationDB / CAP / RabbitMQ / API / Auth / OutboxTestcontainers (digest-pinned images)
ArchitectureDependency direction, module boundaries, banned referencesArchUnitNET

Core paths (transactions/outbox/authentication/data permissions) must include failure paths and negative tests. InMemoryDb cannot replace core integration tests — it never surfaces real SQL dialect differences, connection-pool exhaustion, or transaction isolation behavior.

Data/transaction consistency matrix

Eight scenarios must be automated: a successful Command commits business data; a successful Command writes to the outbox; Result.Failure rolls back; system exceptions roll back; the outbox remains recoverable when RabbitMQ is down; tenant QueryFilters apply; soft-delete QueryFilters apply; asynchronous audit writes never block the business transaction. These eight scenarios are horizontal slices — every newly onboarded module must re-prove them over its own persistence.

API status-code contract

Every error is an RFC 9457 application/problem+json: standard fields type/title/status/detail/instance, plus observability extensions errorCode/errorType/traceId/correlationId/requestId. Mapping funnels through Results/ResultExtensions.ToProblem; endpoints must not fork their own error bodies.

StatusMeaning
400Malformed request/format/basic validation
401Unauthenticated
403No permission or plan disallows
404Resource not found
409Concurrency/idempotency/state conflict
422Business rule not satisfied
429Rate limit triggered
500System exception only

A real 409 response shape follows (errorCode comes from an existing entry in the strongly-typed error catalog; front ends branch on code exactly, while title/detail localize through external i18n):

application/problem+json
{
"type": "https://docs.vnext.ailinkedlaw.com/errors/AI.Message.RequestConflict",
"title": "Request conflict",
"status": 409,
"detail": "A request is already in flight for this conversation; wait for it to finish or cancel it first.",
"instance": "/api/ai/conversations/3fa1c2/messages",
"errorCode": "AI.Message.RequestConflict",
"errorType": "Conflict",
"traceId": "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01",
"correlationId": "6f3a2c9e4d7b4f1a",
"requestId": "0HN7GJQKQV9PF0001"
}

errorCode values follow the {Owner}.{Scenario}.{Reason} three-segment shape and are all registered in 0008-error-catalog.json (currently 1,968 entries); inventing codes outside the catalog is itself a gate violation.

REQ-GATE-001 — zero tolerance for reflection

MakeGenericType(, GetMethod(, Activator.CreateInstance(, and Assembly.GetTypes( are P0 blockers in runtime code; Assembly.GetType( is a P1 warning. Any unregistered IL event in the SqlSugar adapter/Application/Domain auto-escalates to P0 and blocks the merge. The scanner is scripts/build/check-reflection.sh, driven by src/Tooling/BitzOrcas.ReflectionGuard.Cli; paired mutation tests keep the scanner itself honest.

Where reflection is genuinely required, three things must accompany the escape hatch: a // AOT-EXEMPT: annotation stating the business reason, unit tests covering the trimmed scenario, and registration in the exception ledger at docs/architecture/06-aot/0601-aot-exception-ledger.md:

// An unexempted Assembly.GetType is a P1 warning; these lines make it pass the gate.
// AOT-EXEMPT: used only by a startup-time diagnostic probe, with the target type preserved via [DynamicallyAccessedMembers].
var probeType = assembly.GetType(probeTypeName);
// The ledger entry must also state owner, removal condition, and review date; exemption lapses if quarterly review slips.

See ADR 0103 for the source-generation strategy that replaces reflection wholesale.

REQ-GATE-002 — no T-SQL

check-no-tsql.sh scans .cs string literals and hard-blocks "SELECT ", "INSERT ", "UPDATE ", "DELETE ", "EXEC " (P0 unless whitelisted). The whitelist covers the audit Dapper read-side exception and Dapper Query Store read-only queries, reviewed quarterly — and every review asks the same question: why can’t this SQL become an ORM expression yet? Replacements: ORM Lambda/LINQ, Mapperly ProjectToDto(), [BitzTable]-driven ORM configuration.

REQ-GATE-003 — ArchUnitNET assertions and banned packages

The architecture test project BitzOrcas.Architecture.Tests (TngTech.ArchUnitNET + Shouldly + xUnit, roughly a hundred test classes) continuously asserts: Domain has no upstream dependencies; Application has no Infrastructure dependencies; Domain/Application reference no ORM packages; cross-module calls target {Module}.Contracts only; Mapperly appears only in Infrastructure/Workflow; IRepository<T> signatures contain no ORM types; FluentValidation is referenced nowhere.

Technology-choice red lines are mechanized too. The excerpt below shows the core of BannedPackagesTests (comments added for documentation); this prefix list is the executable form of the ADR-level selection matrix:

tests/BitzOrcas.Architecture.Tests/BannedPackagesTests.cs
// Banned prefixes come from ADR 0101/0002/0003; changing this list requires revising the ADR first.
private static readonly string[] BannedPrefixes =
[
"Autofac", // DI goes through built-in Microsoft.Extensions.DependencyInjection
"Newtonsoft.Json", // Serialization goes through System.Text.Json
"Swashbuckle", // OpenAPI docs come from the built-in pipeline and source generation
"AutoMapper", // Mapping goes through compile-time Mapperly generation
"FluentValidation", // Validation goes through the IRequestRule pure-function rule pipeline
"Hangfire",
"Finbuckle",
"MediatR", // CQRS messaging goes through the Mediator source generator
"Mapster",
];
[Fact]
public void Project_Assemblies_Should_Not_Reference_Banned_Libraries()
{
// 1. Enumerate direct references of each controlled assembly and match banned prefixes.
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. A single violation turns fast-gate red, with the full dependency chain in the assertion message.
offenders.ShouldBeEmpty($"Banned packages referenced by project assemblies:\n{string.Join("\n", offenders)}");
}

Sibling red lines include logging isolation (Domain/Application must not touch Serilog) and clock discipline (system time reads go through IAppClock only — a repo-wide scan bans raw DateTime.Now/UtcNow, keeping conflict-of-interest windows and limitation-period calculations deterministically testable). The full list lives in CodingRedLineTests.

Template completion definition

Ten conditions define a complete architecture template: docs include ADR/context-map/selection-matrix/redlines; code includes the 4-layer projects + AppHost + ServiceDefaults + tests; one real business slice runs end-to-end API→DB→outbox; architecture tests block wrong dependencies; integration tests stand up real SQL Server/Redis/RabbitMQ; CI runs build/unit/integration/architecture/trim; the template generates shell + business source only (never copying Framework/Platform/Licensing cores); every Profile completes cold-cache restore/build/test/publish; commercial packages pass signature/hash/SBOM/vulnerability/license gates; Package Entitlement, Runtime License, and tenant Feature Entitlement are layered with online/offline/failure-state tests.

Miss one, and what ships isn’t an architecture template — it’s a source archive that makes the recipient do the homework.

See also

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%