From a17280b9c6c798d9c4a5cebb89e7d0e2619e7fc9 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 29 Aug 2026 20:55:50 -0700 Subject: [PATCH 1/3] Migrate the Hub's Unit-Test Step to Native Microsoft.Testing.Platform On the .NET 10 SDK and later an MTP-based test project refuses to run through the VSTest target, so the reusable validator's `dotnet test --collect:"XPlat Code Coverage"` step fails outright for any caller whose test project has become MTP-based, in practice at xunit.v3 4.0.0. The hub-hosted step is the one file such a caller cannot patch for itself, which left every repo converging onto the reusable workflow with no route forward. Move the step to the MTP-native form driving Microsoft.Testing.Extensions.CodeCoverage, keeping Cobertura output under ./coverage so the existing Codecov upload step reads it unchanged. State the consumer-side prerequisites in WORKFLOW.md D1.6 and mirror them in the csharp.coverage.codecov assert, since the root global.json opt-in, the coverage package, and dropping xunit.runner.visualstudio all live in the consumer's tree and no reusable workflow can place them. Two properties of the invocation are load-bearing, and neither failure reds the job on its own, so both are verified rather than assumed. `--coverage-output` stays unset: pinning one filename gives every test project in the solution the same path, and a two-project probe confirmed the second silently overwrote the first, passing green on half its coverage. The default `.cobertura.xml` that produces is in turn a name codecov-cli's own file finder does not match, its patterns being `*coverage*.*` and an exact `cobertura.xml`, so leaving it alone uploads nothing at all. The step therefore prefixes each report to `coverage-.cobertura.xml`, which keeps the guid that makes reports unique and restores a name the finder matches. Verified on SDK 10.0.400 against a constructed two-project solution: the step writes one report per test project, both packages survive with real per-method data (a covered method at hits="1", an uncovered one at hits="0"), and codecov-cli's finder run over the produced tree returns both files. The same probe established the documented floor. At 18.0.4, what a `>= 18.0.0` range resolves to, the run throws a TypeLoadException against the MTP 2.x that xunit.v3 4.0.0 carries, runs zero tests, and still writes a well-formed Cobertura file reporting full coverage. 18.9.0 is the first release on MTP 2.3.x, where every test project writes into the one shared results directory the invocation names. The prerequisites are stated as binding on a repo whose test project is MTP-based rather than on every C# repo, so a repo still on VSTest holding its existing pin reads as a migration owed rather than as a letter failure the same change calls legitimate. Callers pin this workflow by SHA, so no repo moves until it bumps its own pin. Refs #1088 --- .github/workflows/validate-task.yml | 15 +++++++++++++-- .gitignore | 2 +- WORKFLOW.md | 2 +- spec/project-types.json | 2 +- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml index 74fc5030..22787b99 100644 --- a/.github/workflows/validate-task.yml +++ b/.github/workflows/validate-task.yml @@ -276,10 +276,21 @@ jobs: with: dotnet-version: 10.x - # --collect drives coverlet.collector to emit Cobertura XML into ./coverage//. + # On the .NET 10 SDK and later an MTP-based test project refuses to run through the VSTest target at all, so the --collect:"XPlat Code Coverage" form this step used to carry fails the job outright rather than degrading, which is what a caller hits the moment a dependency bump (xunit.v3 4.0.0 and later, in practice) makes its test project MTP-based. + # --coverage drives Microsoft.Testing.Extensions.CodeCoverage, which writes one report per test project into --results-directory, so the Cobertura XML still lands in ./coverage for the upload step below. + # --coverage-output is deliberately not set, because it pins one filename for every test project in the solution and a caller with more than one then keeps only whichever ran last, passing green on half its coverage. + # The default name it writes instead, .cobertura.xml, is one codecov-cli's own finder does not match (its patterns are *coverage*.* and an exact cobertura.xml), so the reports are prefixed rather than renamed outright, keeping the guid that makes them unique. + # Both halves are load-bearing: pinning the name loses a report, and leaving the name alone uploads nothing at all, and neither reds this job. + # The caller supplies the MTP opt-in and the coverage package itself, in its own root global.json and its own test project, neither of which a reusable workflow can place for it, so a caller that bumps its pin to this commit before migrating is the one failure this step cannot absorb (WORKFLOW.md D1.6). - name: Run unit tests step if: hashFiles('**/*Tests*.csproj') != '' - run: dotnet test --collect:"XPlat Code Coverage" --results-directory ./coverage + run: | + set -Eeuo pipefail + dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage + for report in ./coverage/*.cobertura.xml; do + [ -e "$report" ] || continue + mv "$report" "./coverage/coverage-$(basename "$report")" + done # Report-only: fail_ci_if_error is false so a Codecov hiccup or an absent token never fails the gate. - name: Upload coverage to Codecov step diff --git a/.gitignore b/.gitignore index c74ed796..5a2ea1bb 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ dist/ .pyright/ .mypy_cache/ -# Coverage output (dotnet XPlat/coverlet, Python coverage.py) +# Coverage output (dotnet Microsoft.Testing.Extensions.CodeCoverage, Python coverage.py) coverage/ [Tt]est[Rr]esults/ *.cobertura.xml diff --git a/WORKFLOW.md b/WORKFLOW.md index 1ebe89e8..28fb32b7 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -149,7 +149,7 @@ The required behaviors, organized by domain. Each is a **MUST**, stated as input - **D1.3 Smoke never publishes and never uploads.** Input: `smoke: true`. Output: full compile/lint/test, but no registry/image push, no release, and **no** artifact uploads (every `upload-artifact`, including any aggregation job, is gated `!smoke`). *Prevents: a PR publishing, and orphaned artifacts churning the storage quota.* - **D1.4 Workflow-file changes are not smoke-built.** Input: a PR changing only `.github/workflows/**`. Output: the paths-filter excludes workflow files, so smoke-build skips. *Implication: a workflow-only change is not smoke-built, but actionlint still validates it in CI.* - **D1.5 One required aggregator gates merge.** Input: any PR. Output: a single aggregator job must **succeed**, `needs:` the changes job and the validation job, treat a **skipped** smoke build as pass, and **block** on `failure`/`cancelled`. Its name is ruleset-bound: the job `name:` and the ruleset `context:` are the same string and MUST be renamed together, never independently. *Prevents: a paths-filter error letting a target-changing PR merge unbuilt.* -- **D1.6 Coverage is reported to Codecov (C# and Python).** Input: a C# or Python repo's validation/test job. Output: tests run with coverage collection (`dotnet test --collect:"XPlat Code Coverage"` or `pytest --cov-report=xml`) and a `codecov/codecov-action` step uploads it, **best-effort** (`continue-on-error` and/or `fail_ci_if_error: false`, so a Codecov outage or an absent token never reds the gate). `CODECOV_TOKEN` lives in the repo's **actions** secret store and reaches the reusable validator via `secrets: inherit`. Required for **every** C# and Python repo that has tests. Where this guarantee does not apply (a `lint-only` profile for that type, per `registry/repos.json`), the hub's `spec/secrets.json` `typeMechanisms` mapping is not claimed for that repo, and the absence is not drift. The repo also ships a **`codecov.yml`** that sets the project and patch statuses to **`informational: true`** so a coverage delta never gates a PR (a distinct knob from `fail_ci_if_error`, which only guards the upload step), and excludes intentionally-untested, non-shipped code (an example/demo or benchmark project) from the coverage denominator via `ignore`. A repo may override this to enforce a coverage threshold where its quality bar requires it. Coverage output is a build artifact, so `.gitignore` excludes it (e.g. `coverage/` and `*.cobertura.xml`, with `.gitignore` the full source of truth) so a blanket `git add -A` won't stage the untracked output. *Prevents: coverage silently going unreported; a stale, unused token; a coverage regression blocking an unrelated PR; a coverage artifact committed by a blanket add.* +- **D1.6 Coverage is reported to Codecov (C# and Python).** Input: a C# or Python repo's validation/test job. Output: tests run with coverage collection (`dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage` or `pytest --cov-report=xml`) and a `codecov/codecov-action` step uploads it, **best-effort** (`continue-on-error` and/or `fail_ci_if_error: false`, so a Codecov outage or an absent token never reds the gate). `CODECOV_TOKEN` lives in the repo's **actions** secret store and reaches the reusable validator via `secrets: inherit`. Required for **every** C# and Python repo that has tests. That C# invocation runs under **Microsoft.Testing.Platform**, which an MTP-based test project on the .NET 10 SDK and later requires, since running one through the VSTest target fails outright. A repo whose test project is MTP-based, in practice any repo on xunit.v3 4.0.0 or later, therefore also ships a root **`global.json`** declaring `{"test": {"runner": "Microsoft.Testing.Platform"}}`, references **`Microsoft.Testing.Extensions.CodeCoverage`** at **18.9.0 or later** in place of `coverlet.collector`, whose VSTest data collector MTP ignores without failing, and drops `xunit.runner.visualstudio`, the VSTest adapter MTP replaces. A repo whose test project is not yet MTP-based keeps the VSTest collector and its existing pin on the reusable validator, and that lagging state is a migration still owed rather than drift, until its own bump makes the project MTP-based and forces the move. The version floor is load-bearing rather than cautionary. Below 18.1.0 the extension is built against Microsoft.Testing.Platform 1.x, and an 18.0.x resolution, which is what a `>= 18.0.0` range picks, throws a `TypeLoadException` against the 2.x platform xunit.v3 4.0.0 carries, runs zero tests, and still writes a well-formed Cobertura file reporting full coverage, so only the non-zero exit says the run reported nothing. 18.9.0 is the first release on Microsoft.Testing.Platform 2.3.x, where every test project writes into the one shared `--results-directory` the invocation names rather than resolving that relative path per project. Two details of the invocation are equally load-bearing, and neither failure reds the job on its own. `--coverage-output` stays unset, because pinning one filename gives every test project in the solution the same path and a repo with more than one then keeps only whichever ran last. The default name that produces, `.cobertura.xml`, is in turn one `codecov-cli`'s own file finder does not match, its patterns being `*coverage*.*` and an exact `cobertura.xml`, so the validator prefixes each report to `coverage-.cobertura.xml` before the upload step reads the directory. Where this guarantee does not apply (a `lint-only` profile for that type, per `registry/repos.json`), the hub's `spec/secrets.json` `typeMechanisms` mapping is not claimed for that repo, and the absence is not drift. The repo also ships a **`codecov.yml`** that sets the project and patch statuses to **`informational: true`** so a coverage delta never gates a PR (a distinct knob from `fail_ci_if_error`, which only guards the upload step), and excludes intentionally-untested, non-shipped code (an example/demo or benchmark project) from the coverage denominator via `ignore`. A repo may override this to enforce a coverage threshold where its quality bar requires it. Coverage output is a build artifact, so `.gitignore` excludes it (e.g. `coverage/` and `*.cobertura.xml`, with `.gitignore` the full source of truth) so a blanket `git add -A` won't stage the untracked output. *Prevents: coverage silently going unreported; a test project stranded on a runner the current SDK refuses; a stale, unused token; a coverage regression blocking an unrelated PR; a coverage artifact committed by a blanket add.* ### D2 - Input/State Validation at Entry diff --git a/spec/project-types.json b/spec/project-types.json index 74313ab2..71df3cbf 100644 --- a/spec/project-types.json +++ b/spec/project-types.json @@ -11,7 +11,7 @@ { "id": "csharp.editorconfig.ruleblock", "verdict": "letter", "assert": ".editorconfig carries the shared [*.cs] plus ReSharper rule block.", "intentRef": "CODESTYLE.md" }, { "id": "csharp.analyzers.zerowarnings", "verdict": "intent", "assert": "Analyzer severities are enforced; warnings are not relaxed or suppressed wholesale.", "intentRef": "CODESTYLE.md" }, { "id": "csharp.centralconfig.props", "verdict": "letter", "assert": "Shared MSBuild configuration is centralized at the repo root: Directory.Build.props carries the common analyzer and warning properties (the Zero Warnings set), and Directory.Packages.props enables ManagePackageVersionsCentrally with every dependency version declared once - a csproj carries only project-specific properties and versionless PackageReference items.", "intentRef": "CODESTYLE.md" }, - { "id": "csharp.coverage.codecov", "verdict": "letter", "assert": "The unit-test job collects coverage (dotnet test --collect:\"XPlat Code Coverage\" --results-directory ./coverage) and uploads it to Codecov via codecov/codecov-action, best-effort (fail_ci_if_error: false so a Codecov outage or an absent token never reds the gate). CODECOV_TOKEN is stored in the repo actions secrets and reaches the reusable validator via secrets: inherit. Required for every C# repo with tests.", "intentRef": "WORKFLOW.md", "minProfile": "build" } + { "id": "csharp.coverage.codecov", "verdict": "letter", "assert": "The unit-test job collects coverage (dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage, with the output filename left unset so each test project writes its own report rather than overwriting a shared one) and uploads it to Codecov via codecov/codecov-action, best-effort (fail_ci_if_error: false so a Codecov outage or an absent token never reds the gate). CODECOV_TOKEN is stored in the repo actions secrets and reaches the reusable validator via secrets: inherit. The validator prefixes each report to coverage-.cobertura.xml before uploading, since codecov-cli does not match the default name. Where the repo's test project is MTP-based (xunit.v3 4.0.0 or later), it also carries a root global.json declaring test.runner as Microsoft.Testing.Platform, references Microsoft.Testing.Extensions.CodeCoverage at 18.9.0 or later rather than coverlet.collector, and drops xunit.runner.visualstudio. A test project not yet MTP-based keeps the VSTest collector and its existing pin, which is a migration owed rather than drift. Required for every C# repo with tests.", "intentRef": "WORKFLOW.md", "minProfile": "build" } ] }, "nuget": { From 32021cf1728eacf0aba9ca3e8adf63a52343a52f Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 30 Aug 2026 06:40:41 -0700 Subject: [PATCH 2/3] Trim the Unit-Test Step's Comment Block to the Constraints the Code Cannot Carry The block ran to six lines of migration history and a WORKFLOW.md citation, against the fleet's one-line default, its second-line-for-a-constraint exception, and its rule that governance is not echoed inline. It also framed the command as what the step used to carry, which documents the change rather than the current behavior. Keep only the two constraints the code cannot express, that --coverage-output stays unset so one test project does not overwrite another, and that the default report name is one codecov-cli does not match. D1.6 keeps the migration rationale and the consumer-side prerequisites. --- .github/workflows/validate-task.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/validate-task.yml b/.github/workflows/validate-task.yml index 22787b99..afec365d 100644 --- a/.github/workflows/validate-task.yml +++ b/.github/workflows/validate-task.yml @@ -276,12 +276,8 @@ jobs: with: dotnet-version: 10.x - # On the .NET 10 SDK and later an MTP-based test project refuses to run through the VSTest target at all, so the --collect:"XPlat Code Coverage" form this step used to carry fails the job outright rather than degrading, which is what a caller hits the moment a dependency bump (xunit.v3 4.0.0 and later, in practice) makes its test project MTP-based. - # --coverage drives Microsoft.Testing.Extensions.CodeCoverage, which writes one report per test project into --results-directory, so the Cobertura XML still lands in ./coverage for the upload step below. - # --coverage-output is deliberately not set, because it pins one filename for every test project in the solution and a caller with more than one then keeps only whichever ran last, passing green on half its coverage. - # The default name it writes instead, .cobertura.xml, is one codecov-cli's own finder does not match (its patterns are *coverage*.* and an exact cobertura.xml), so the reports are prefixed rather than renamed outright, keeping the guid that makes them unique. - # Both halves are load-bearing: pinning the name loses a report, and leaving the name alone uploads nothing at all, and neither reds this job. - # The caller supplies the MTP opt-in and the coverage package itself, in its own root global.json and its own test project, neither of which a reusable workflow can place for it, so a caller that bumps its pin to this commit before migrating is the one failure this step cannot absorb (WORKFLOW.md D1.6). + # --coverage-output stays unset because pinning one filename gives every test project in the solution the same path, and the last to finish overwrites the rest. + # The default .cobertura.xml written instead is a name codecov-cli's finder does not match, so each report is prefixed rather than renamed, keeping the guid that makes it unique. - name: Run unit tests step if: hashFiles('**/*Tests*.csproj') != '' run: | From e114ae28c660e13e7295da43f5ff6df3a0fd39de Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sun, 30 Aug 2026 06:43:15 -0700 Subject: [PATCH 3/3] Recast D1.6's Prevents Clause Without Semicolons The clause separated its items with semicolons, which the exemption for a list carrying its own commas permitted, since 'a stale, unused token' has one. Recasting that item to 'a stale and unused token' removes the internal comma, so the list no longer needs the exemption and reads as a plain comma series. This also matches the dominant form in the file, where 21 of 23 Prevents clauses already use commas. --- WORKFLOW.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WORKFLOW.md b/WORKFLOW.md index 28fb32b7..492f7cad 100644 --- a/WORKFLOW.md +++ b/WORKFLOW.md @@ -149,7 +149,7 @@ The required behaviors, organized by domain. Each is a **MUST**, stated as input - **D1.3 Smoke never publishes and never uploads.** Input: `smoke: true`. Output: full compile/lint/test, but no registry/image push, no release, and **no** artifact uploads (every `upload-artifact`, including any aggregation job, is gated `!smoke`). *Prevents: a PR publishing, and orphaned artifacts churning the storage quota.* - **D1.4 Workflow-file changes are not smoke-built.** Input: a PR changing only `.github/workflows/**`. Output: the paths-filter excludes workflow files, so smoke-build skips. *Implication: a workflow-only change is not smoke-built, but actionlint still validates it in CI.* - **D1.5 One required aggregator gates merge.** Input: any PR. Output: a single aggregator job must **succeed**, `needs:` the changes job and the validation job, treat a **skipped** smoke build as pass, and **block** on `failure`/`cancelled`. Its name is ruleset-bound: the job `name:` and the ruleset `context:` are the same string and MUST be renamed together, never independently. *Prevents: a paths-filter error letting a target-changing PR merge unbuilt.* -- **D1.6 Coverage is reported to Codecov (C# and Python).** Input: a C# or Python repo's validation/test job. Output: tests run with coverage collection (`dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage` or `pytest --cov-report=xml`) and a `codecov/codecov-action` step uploads it, **best-effort** (`continue-on-error` and/or `fail_ci_if_error: false`, so a Codecov outage or an absent token never reds the gate). `CODECOV_TOKEN` lives in the repo's **actions** secret store and reaches the reusable validator via `secrets: inherit`. Required for **every** C# and Python repo that has tests. That C# invocation runs under **Microsoft.Testing.Platform**, which an MTP-based test project on the .NET 10 SDK and later requires, since running one through the VSTest target fails outright. A repo whose test project is MTP-based, in practice any repo on xunit.v3 4.0.0 or later, therefore also ships a root **`global.json`** declaring `{"test": {"runner": "Microsoft.Testing.Platform"}}`, references **`Microsoft.Testing.Extensions.CodeCoverage`** at **18.9.0 or later** in place of `coverlet.collector`, whose VSTest data collector MTP ignores without failing, and drops `xunit.runner.visualstudio`, the VSTest adapter MTP replaces. A repo whose test project is not yet MTP-based keeps the VSTest collector and its existing pin on the reusable validator, and that lagging state is a migration still owed rather than drift, until its own bump makes the project MTP-based and forces the move. The version floor is load-bearing rather than cautionary. Below 18.1.0 the extension is built against Microsoft.Testing.Platform 1.x, and an 18.0.x resolution, which is what a `>= 18.0.0` range picks, throws a `TypeLoadException` against the 2.x platform xunit.v3 4.0.0 carries, runs zero tests, and still writes a well-formed Cobertura file reporting full coverage, so only the non-zero exit says the run reported nothing. 18.9.0 is the first release on Microsoft.Testing.Platform 2.3.x, where every test project writes into the one shared `--results-directory` the invocation names rather than resolving that relative path per project. Two details of the invocation are equally load-bearing, and neither failure reds the job on its own. `--coverage-output` stays unset, because pinning one filename gives every test project in the solution the same path and a repo with more than one then keeps only whichever ran last. The default name that produces, `.cobertura.xml`, is in turn one `codecov-cli`'s own file finder does not match, its patterns being `*coverage*.*` and an exact `cobertura.xml`, so the validator prefixes each report to `coverage-.cobertura.xml` before the upload step reads the directory. Where this guarantee does not apply (a `lint-only` profile for that type, per `registry/repos.json`), the hub's `spec/secrets.json` `typeMechanisms` mapping is not claimed for that repo, and the absence is not drift. The repo also ships a **`codecov.yml`** that sets the project and patch statuses to **`informational: true`** so a coverage delta never gates a PR (a distinct knob from `fail_ci_if_error`, which only guards the upload step), and excludes intentionally-untested, non-shipped code (an example/demo or benchmark project) from the coverage denominator via `ignore`. A repo may override this to enforce a coverage threshold where its quality bar requires it. Coverage output is a build artifact, so `.gitignore` excludes it (e.g. `coverage/` and `*.cobertura.xml`, with `.gitignore` the full source of truth) so a blanket `git add -A` won't stage the untracked output. *Prevents: coverage silently going unreported; a test project stranded on a runner the current SDK refuses; a stale, unused token; a coverage regression blocking an unrelated PR; a coverage artifact committed by a blanket add.* +- **D1.6 Coverage is reported to Codecov (C# and Python).** Input: a C# or Python repo's validation/test job. Output: tests run with coverage collection (`dotnet test --coverage --coverage-output-format cobertura --results-directory ./coverage` or `pytest --cov-report=xml`) and a `codecov/codecov-action` step uploads it, **best-effort** (`continue-on-error` and/or `fail_ci_if_error: false`, so a Codecov outage or an absent token never reds the gate). `CODECOV_TOKEN` lives in the repo's **actions** secret store and reaches the reusable validator via `secrets: inherit`. Required for **every** C# and Python repo that has tests. That C# invocation runs under **Microsoft.Testing.Platform**, which an MTP-based test project on the .NET 10 SDK and later requires, since running one through the VSTest target fails outright. A repo whose test project is MTP-based, in practice any repo on xunit.v3 4.0.0 or later, therefore also ships a root **`global.json`** declaring `{"test": {"runner": "Microsoft.Testing.Platform"}}`, references **`Microsoft.Testing.Extensions.CodeCoverage`** at **18.9.0 or later** in place of `coverlet.collector`, whose VSTest data collector MTP ignores without failing, and drops `xunit.runner.visualstudio`, the VSTest adapter MTP replaces. A repo whose test project is not yet MTP-based keeps the VSTest collector and its existing pin on the reusable validator, and that lagging state is a migration still owed rather than drift, until its own bump makes the project MTP-based and forces the move. The version floor is load-bearing rather than cautionary. Below 18.1.0 the extension is built against Microsoft.Testing.Platform 1.x, and an 18.0.x resolution, which is what a `>= 18.0.0` range picks, throws a `TypeLoadException` against the 2.x platform xunit.v3 4.0.0 carries, runs zero tests, and still writes a well-formed Cobertura file reporting full coverage, so only the non-zero exit says the run reported nothing. 18.9.0 is the first release on Microsoft.Testing.Platform 2.3.x, where every test project writes into the one shared `--results-directory` the invocation names rather than resolving that relative path per project. Two details of the invocation are equally load-bearing, and neither failure reds the job on its own. `--coverage-output` stays unset, because pinning one filename gives every test project in the solution the same path and a repo with more than one then keeps only whichever ran last. The default name that produces, `.cobertura.xml`, is in turn one `codecov-cli`'s own file finder does not match, its patterns being `*coverage*.*` and an exact `cobertura.xml`, so the validator prefixes each report to `coverage-.cobertura.xml` before the upload step reads the directory. Where this guarantee does not apply (a `lint-only` profile for that type, per `registry/repos.json`), the hub's `spec/secrets.json` `typeMechanisms` mapping is not claimed for that repo, and the absence is not drift. The repo also ships a **`codecov.yml`** that sets the project and patch statuses to **`informational: true`** so a coverage delta never gates a PR (a distinct knob from `fail_ci_if_error`, which only guards the upload step), and excludes intentionally-untested, non-shipped code (an example/demo or benchmark project) from the coverage denominator via `ignore`. A repo may override this to enforce a coverage threshold where its quality bar requires it. Coverage output is a build artifact, so `.gitignore` excludes it (e.g. `coverage/` and `*.cobertura.xml`, with `.gitignore` the full source of truth) so a blanket `git add -A` won't stage the untracked output. *Prevents: coverage silently going unreported, a test project stranded on a runner the current SDK refuses, a stale and unused token, a coverage regression blocking an unrelated PR, and a coverage artifact committed by a blanket add.* ### D2 - Input/State Validation at Entry