Table of Contents

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:

  • TargetFramework set to net10.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 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 / dbProvider combinations 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 ContosoSecurityPortal with the selected authentication and database options.
  • Validates the generated scaffold against eng/scaffold-manifest.default.json.
  • Runs eng/Assert-TemplateOptionScaffold.ps1 to 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:

  • .editorconfig
  • global.json
  • Directory.Build.props
  • Directory.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:

  1. Run the workflow manually (workflow_dispatch) with egress_policy set to block.
  2. Compare the Harden-Runner insights for that run against the allow-lists and add any endpoint that was blocked but is legitimately required.
  3. Repeat until a full run, including the cross-platform smoke test and option matrix, passes in block mode.
  4. Change the EGRESS_POLICY default in ci.yml to block.

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.