Build Quality and Reproducibility
This repository uses a shared build policy so local development, CI validation, container builds, template packaging, and scaffolded consumer output follow the same baseline expectations.
SDK and Test Runner Policy
The repository pins the .NET SDK and explicitly selects Microsoft.Testing.Platform through global.json.
Current policy:
{
"sdk": {
"version": "10.0.400",
"rollForward": "latestPatch",
"allowPrerelease": false
},
"test": {
"runner": "Microsoft.Testing.Platform"
}
}
The pinned SDK feature band keeps local and CI builds aligned. latestPatch allows patch-level SDK servicing updates within the selected feature band without silently moving to a newer feature band.
The explicit test runner keeps repository tests and generated-project tests on Microsoft.Testing.Platform instead of relying on SDK inference. Test projects reference the standard xunit.v3 package so its Microsoft.Testing.Platform integration remains active. Coverage uses coverlet.MTP through scripts/Invoke-MtpCoverage.ps1, keeping coverage arguments on the MTP extension path across Windows and Unix runners.
CI workflows use actions/setup-dotnet with global-json-file: global.json so the repository SDK policy remains the single source of truth.
Central Package Management
NuGet package versions are centralized in Directory.Packages.props by enabling:
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
Project files should reference packages without inline Version attributes unless a documented exception is required.
This keeps package drift visible, makes dependency review easier, and ensures scaffolded consumer output can restore with the same package policy as the source repository.
NuGet Lock Files
RestorePackagesWithLockFile is enabled for every project. Each project therefore keeps a packages.lock.json beside its project file, including projects emitted by the dotnet new template.
Repository, generated-scaffold, and Docker restores use dotnet restore --locked-mode. CI first verifies that every project has a lock file so a missing file cannot be silently generated during a supposedly locked restore. After an intentional dependency change, run dotnet restore --force-evaluate, review the lock-file diff, and commit it with the dependency update.
Shared Build Properties
Common build settings live in Directory.Build.props.
The shared policy includes:
TargetFrameworkset tonet10.0.- Nullable reference types enabled.
- Implicit usings enabled.
- Centralized version metadata.
- Deterministic builds.
- Embedded debug symbols.
- Repository metadata for Source Link.
- .NET analyzers enabled.
- Code style enforcement during build.
- XML documentation generation.
Deterministic Builds
The build policy enables deterministic output where appropriate:
<Deterministic>true</Deterministic>
<ContinuousIntegrationBuild Condition="'$(GITHUB_ACTIONS)' == 'true'">true</ContinuousIntegrationBuild>
<DebugType>embedded</DebugType>
CI also passes /p:ContinuousIntegrationBuild=true during release builds, template packing, scaffolded output validation, documentation builds, and container publish builds.
Source Link
Source Link support is configured with repository metadata and the Microsoft.SourceLink.GitHub package.
Project files that produce assemblies should include:
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="all" />
The CI checkout uses full history with fetch-depth: 0 for build jobs that need repository metadata.
Analyzer and Formatting Policy
Analyzer severity and code-style preferences are defined in .editorconfig.
CI enforces formatting with:
dotnet format ./NetCoreApplicationTemplate.slnx --verify-no-changes --verbosity minimal
Analyzer settings are intentionally production-oriented but not configured as global warnings-as-errors. This allows the template to remain practical while still surfacing reliability, security, performance, usage, code-quality, and style issues during builds and IDE development.
Release Build Quality Gates
Before a release, the expected validation path is:
dotnet build --configuration Release --no-restore /p:ContinuousIntegrationBuild=true
dotnet format ./NetCoreApplicationTemplate.slnx --verify-no-changes --verbosity minimal
dotnet test --configuration Release --no-build --verbosity normal /p:ContinuousIntegrationBuild=true
dotnet pack ./eng/NetCoreApplicationTemplate.Template.csproj --configuration Release --output ./artifacts/template-package /p:ContinuousIntegrationBuild=true
The CI workflow additionally:
- Runs dependency review on pull requests.
- Generates coverage reports.
- Enforces the configured global and security-critical coverage thresholds.
- Runs CodeQL analysis.
- Packs and installs the template package.
- Scaffolds a consumer project.
- Verifies expected scaffolded files and excluded maintainer files.
- Builds and tests scaffolded output on Linux, Windows, and macOS.
- Builds and tests all six supported
authProvider/dbProvidercombinations on Linux.
Template Option Matrix
The template supports two authentication choices (cookie, none) and three database choices (sqlite, sqlserver, none). CI validates the complete six-combination cross product on Linux in addition to the default cross-platform smoke test.
Every matrix entry:
- Packs and installs the template package.
- Generates
ContosoSecurityPortalwith the selected authentication and database options. - Validates the generated scaffold against
eng/scaffold-manifest.default.json. - Runs
eng/Assert-TemplateOptionScaffold.ps1to verify authentication, fallback-authorization, data-provider, connection-string, and migration-content expectations. - Verifies generated package lock files.
- Restores with locked mode, builds in Release configuration, and runs the generated tests.
The matrix is intentionally Linux-only because the default cookie + sqlite scaffold already retains Linux, Windows, and macOS coverage. Runtime-heavy Docker build, Compose, and health-probe validation also remains on that representative default Linux smoke path rather than running for every option combination.
Matrix concurrency is capped at three entries and every matrix job has a 20-minute timeout. This bounds runner pressure and prevents a single stalled combination from extending CI indefinitely while still allowing the six combinations to complete in parallel waves.
Scaffolded Output
The generated template intentionally includes:
.editorconfigglobal.jsonDirectory.Build.propsDirectory.Packages.props
These files are part of the consumer build contract because package versions and build quality settings are centralized at the repository root.
The scaffolded global.json retains the repository's explicit Microsoft.Testing.Platform selection, and the generated test project uses xunit.v3 under that runner.
Coverage Policy
The CI coverage gate requires at least 75% repository line coverage as a minimum safety net. CI enforces the threshold through COVERAGE_THRESHOLD in .github/workflows/ci.yml, and it applies to the repository as a standing policy rather than to a particular release. Contract-level integration tests protect advertised runtime behavior directly, while the global threshold prevents broad coverage regression without forcing low-value tests. Historical note: this 75% threshold was originally documented as the v1.0.2 coverage gate.
Security-critical files also have a stricter file-level coverage gate. This second gate exists because global line coverage can hide concentrated risk in files responsible for error handling, request classification, identity resolution, audit attribution, security headers, forwarded headers, rate limiting, and persistence normalization.
The protected file list is maintained in:
eng/security-critical-coverage.json
The CI assertion script is:
eng/Assert-SecurityCriticalCoverage.ps1
The per-file gate evaluates ReportGenerator's generated Cobertura output and fails CI with actionable file-level messages when a protected file falls below its configured line or branch threshold.
The per-file gate is stricter than the repository gate by construction:
| Setting | Value | Rule |
|---|---|---|
defaultMinimumLineCoverage |
75% | Applies to protected files without an explicit line floor. Must not be below the repository line gate (COVERAGE_THRESHOLD). |
defaultMinimumBranchCoverage |
60% | Applies to protected files without an explicit branch floor. The repository gate has no branch threshold, so this is the only branch floor. |
Per-file minimumLineCoverage |
Optional | Must not be below the repository line gate. |
Per-file minimumBranchCoverage |
Optional | Must not be below defaultMinimumBranchCoverage. |
CI passes COVERAGE_THRESHOLD to the script as -RepositoryLineCoverageThreshold, and the script fails when any effective line floor is below it or when a per-file branch floor is below the default. This keeps a protected file from silently carrying a weaker requirement than an unprotected one. Run the script without -RepositoryLineCoverageThreshold for a local diagnostic report that skips the floor check.
Protected files should be added when they control security, trust boundaries, request identity, safe failure, audit attribution, or persistence safety. Raising a file above the defaults is encouraged where tests support it. Lowering a protected threshold, including the defaults, is a quality-gate change and should include a reason in the pull request.
Runner Egress Policy
CI hardens each Linux job with Harden-Runner. The policy is driven by the EGRESS_POLICY workflow environment variable, which defaults to audit: outbound calls are recorded but never blocked.
ci.yml also carries the allow-lists the jobs are expected to need:
| Variable | Used by | Covers |
|---|---|---|
ALLOWED_ENDPOINTS_DOTNET |
Build validation, template option matrix | GitHub Actions infrastructure, the .NET SDK installer, and NuGet restore |
ALLOWED_ENDPOINTS_SMOKE |
Template smoke test | The same endpoints plus the container registries used by the scaffolded Docker build and Compose run |
Harden-Runner ignores allowed-endpoints while the policy is audit, so the lists are inert until block mode is used.
To move to blocking:
- Run the workflow manually (
workflow_dispatch) withegress_policyset toblock. - Compare the Harden-Runner insights for that run against the allow-lists and add any endpoint that was blocked but is legitimately required.
- Repeat until a full run, including the cross-platform smoke test and option matrix, passes in block mode.
- Change the
EGRESS_POLICYdefault inci.ymltoblock.
Harden-Runner enforces policy on Linux runners only. The Windows and macOS smoke-test legs remain unenforced regardless of the policy value.
Dependency Upgrade Policy
Dependency updates should be reviewed by impact level.
| Update Type | Review Expectation |
|---|---|
| Security updates | Review and merge promptly after CI passes unless the update causes a documented compatibility break. |
| Patch updates | Prefer merging after CI, template smoke tests, and dependency review pass. |
| Minor updates | Review release notes, then merge after CI and smoke tests pass. |
| Major updates | Treat as compatibility work. Review release notes, migration guides, runtime behavior, and scaffolded output before merging. |
| Runtime-sensitive updates | Manually review authentication, EF Core, middleware, logging, telemetry, container, and workflow dependencies even for patch or minor updates. |
Dependabot groups related dependencies where practical, but grouped dependency pull requests still require human review before merging.
Version and Release Notes
The repository version is centralized in build metadata. Release notes should identify any change that affects:
- SDK feature-band policy.
- Target framework.
- Central package management.
- Analyzer severity.
- CI quality gates.
- Template scaffolded build files.
- Dependency policy.
After v1.0.0, changes to these items should be treated as release-surface changes because they can affect downstream generated applications.