Canonical-protected: C1 drift + nullable + S1 + D6 + D8 + A1 + CI3 + T1 + T3#135
Conversation
Template-drift resolution for IEnumerable-Extensions — the protected half. These files trip the pr.yaml "Detect .NET Projects" guard, so they are split into their own PR that a maintainer merges via admin bypass: - .editorconfig, BannedSymbols.txt - .github/workflows/pr.yaml, codeql.yaml, docfx.yaml Part of #106. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR is part of the C1 template-drift re-sync work and updates protected repo configuration/workflow files (intended to be merged via admin bypass) to align with the canonical template while tightening some CI security behaviors.
Changes:
- PR workflow: hardens gitleaks installation (checksum verification), restores .NET workloads, and replaces grep-based TFM parsing with MSBuild property evaluation.
- DocFX workflow: refactors gh-pages deployment to avoid embedding tokens in remote URLs and improves cleanup behavior.
- Misc config: updates
.editorconfigPowerShell guidance and syncsBannedSymbols.txtheader text.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
BannedSymbols.txt |
Header text synced toward template format. |
.editorconfig |
Removes PowerShell-specific EOL/BOM override and replaces with explanatory comments. |
.github/workflows/pr.yaml |
CI hardening + workload restore + more robust TFM detection + test-stage behavior updates. |
.github/workflows/codeql.yaml |
Adds workload restore step before build for CodeQL runs. |
.github/workflows/docfx.yaml |
Refactors gh-pages deploy authentication/cleanup logic. |
Comments suppressed due to low confidence (3)
.github/workflows/pr.yaml:714
- This step always passes
--settings coverlet.runsettingsfor .NET 5+ frameworks, butcoverlet.runsettingsis not present in the repo. That will causedotnet testto fail. Make the runsettings file optional (only pass it when it exists), consistent with scripts/build-pr.ps1.
if ($fw -match '^net([5-9]|[1-9][0-9]+)\.') {
dotnet test $testProj.FullName `
--configuration Release `
--framework $fw `
--collect:"XPlat Code Coverage" `
--settings coverlet.runsettings `
--results-directory "./TestResults" `
--logger "console;verbosity=normal"
.github/workflows/pr.yaml:1050
dotnet test --settings coverlet.runsettingswill fail ifcoverlet.runsettingsis not present in the repo (it currently isn't). Make the runsettings file optional so the workflow doesn't hard-fail on a missing file.
dotnet test "$test_proj" \
--configuration Release \
--framework "$fw" \
--collect:"XPlat Code Coverage" \
--settings coverlet.runsettings \
--results-directory "./TestResults" \
--logger "console;verbosity=normal" || exit 1
.github/workflows/docfx.yaml:365
exit 1here is inside thetry {}block and may prevent thefinally {}cleanup (including unsetting the globalhttp.extraheader). Usethrowso the step fails while still guaranteeing thefinallyblock runs.
if (-not (Test-Path '.github/version-picker-template.html')) {
Write-Error "Error: .github/version-picker-template.html not found; cannot generate root index.html."
exit 1
}
Add <Nullable>enable</Nullable> to the root Directory.Build.props and remove the now-redundant per-project <Nullable> lines from every SDK-style csproj. Nullable reference types are configured in one place; a newly added project inherits the setting automatically. Both halves ride this protected branch so they merge atomically — the Directory.Build.props addition and the csproj removals land together, so there is never a window where nullable reference types are off. Legacy non-SDK project files do not import Directory.Build.props and are left untouched with their explicit settings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Scope added to this PR — nullable consolidation. |
Legacy non-SDK .csproj files explicitly import Microsoft.Common.props,
so they DO inherit Directory.Build.props — the earlier unconditional
<Nullable>enable</Nullable> reached projects it should not have:
* F# (.fsproj) / VB (.vbproj) projects — now excluded by conditioning
the property on '$(MSBuildProjectExtension)' == '.csproj'.
* legacy non-SDK C# example projects (C# 7.3, no nullable support) —
given an explicit <Nullable>disable</Nullable> opt-out, restoring
their pre-hoist state. These are the documented C5 carve-outs.
SDK-style C# projects are unaffected — they still inherit enable from
the single Directory.Build.props.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds queries: security-extended to the CodeQL init step so the broader security query pack runs on top of the default queries. Slightly longer scans, materially more security coverage. Initiative S1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New .github/workflows/stryker.yaml runs Stryker.NET against the repo's test projects on workflow_dispatch and a weekly schedule. The workflow is a no-op until a stryker-config.json is added at repo root or under tests/<project>/ — this commit is the canonical infrastructure; per-repo Stryker config is the follow-up. Initiative T3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a verify-docs-build job to release.yaml that runs DocFX without deploying (metadata + build + output check). publish-nuget now needs [pack-and-validate, verify-docs-build] so a broken docs build blocks the release before the NuGet package goes live. If a repo has no docfx_project/docfx.json, the job no-ops with a notice; this is the canonical infrastructure, with per-repo docs coverage tracked separately. Initiative D8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Scope additions to this PR — S1 + D8 stacked. The canonical-protected branch now also carries:
All four logical changes — C1 drift re-sync, nullable consolidation, S1, D8 — ride this single protected PR per the standing-branch model and merge atomically via admin bypass. |
Add a guard step to docfx.yaml that fetches the currently-deployed versions.json from gh-pages and confirms the newly-generated one has at least as many entries AND retains every previously-published version label. Aborts the deploy if the version selector would shrink or lose entries. If no existing versions.json is found (first deploy), the step no-ops with a notice. Initiative D6. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Scope addition to this PR — D6 stacked. Adds a |
Add the Microsoft.CodeAnalysis.PublicApiAnalyzers package plus opt-in AdditionalFiles globbing for PublicAPI.Shipped.txt / Unshipped.txt to the root Directory.Build.props. The AdditionalFiles use Exists() conditions, so the analyzer activates per-project only when those files are present. Library projects opt in by dropping the two text files into the src directory; test, example, and benchmark projects stay dormant. Per-repo enablement (populate Unshipped.txt with the current public API surface) is tracked as a separate follow-up maintenance issue. Initiative A1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add fleet-canonical defaults to root Directory.Build.props: - Authors / Company / Copyright (uniform across the fleet; per-csproj values still win where set explicitly). - RepositoryType=git, PublishRepositoryUrl=true. - IncludeSymbols=true + SymbolPackageFormat=snupkg so .snupkg ships with every .nupkg. - EmbedUntrackedSources=true to capture generated sources in PDBs. - ContinuousIntegrationBuild=true under $(CI)=true (deterministic build flag, set by GitHub Actions). - Microsoft.SourceLink.GitHub package so debuggers can step from NuGet-installed code straight to GitHub source. Repo-specific NuGet fields (Description, PackageTags, PackageProjectUrl, RepositoryUrl, PackageLicenseExpression, PackageReadmeFile) stay in per-src csproj where they belong and are tracked as per-repo follow-up maintenance issues. Initiative CI3. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a docfx.yaml step that runs the test suite with Cobertura coverage collection and generates a ReportGenerator HTML report into docfx_project/_site/coverage/ before the deploy step. The published docs site gains a /coverage/ subpath alongside the existing /api/. continue-on-error keeps a coverage failure from blocking the docs deploy; if no test projects are present the step no-ops. Initiative T1. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy-Item -Force overwrites files that exist in both source and destination, but doesn't remove destination-only files. Over multiple releases — if a docs page or asset is dropped from docfx output — the stale file lingers indefinitely in gh-pages, served alongside the current docs. Adding a clear-before-copy step for both versions/<VERSION_DIR> and versions/latest. Preserves the directory entry itself so per-release git diffs stay clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier "Restore dependencies" step already does the restore, so the implicit restore inside dotnet test is wasted I/O. Adding --no-restore alongside the existing --no-build to skip both. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The D6 "Verify previous versions preserved in versions.json" guard
ran whenever deploy_to_pages != false. But the deploy step only
touches the root versions.json when deploy_as_latest is ALSO true:
- deploy_to_pages=false → dry-run, nothing deploys
- deploy_to_pages=true + deploy_as_latest=false → rebuild a single
older version; deploy writes only versions/<dir>/, doesn't touch
the root
- deploy_to_pages=true + deploy_as_latest=true → full deploy that
overwrites the root versions.json
The guard exists to protect that root file. In the rebuild-an-older-
version case there's nothing for the guard to protect, but it still
fetched the live Pages versions.json and compared — meaning a
transient Pages fetch/parse error would block a legitimate rebuild.
Tightening the if condition so the guard only runs in the third case
(both inputs true).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Write-Error prefixes its output with PowerShell error formatting (file/line/category metadata), so a literal ::error::... payload no longer starts the line — and Actions' workflow-command parser looks for ::error:: at the BEGINNING of a stream line. The result was that the missing-versions.json failure produced a red error in the log but no annotation marker on the run summary. Switching to Write-Host (literal output, no PowerShell prefixing) plus the existing exit 1 keeps the annotation visible and still fails the step. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
My earlier Stage 1 hardening kept the integer-only regex [0-9]+% and
the bash integer comparator -lt. ReportGenerator typically emits
decimals like "92.3%" — the regex missed those, and the new
matched_count=0 guard would then fail jobs whose Summary.txt had only
decimal rows. Even after broadening the regex to accept decimals, bash
[ errors with "integer expression expected" on a non-integer value.
Two fixes:
1. Regex now matches [0-9]+(\.[0-9]+)?% so decimal percents
count toward matched_count.
2. Percent is floored to an integer via awk '{print int($1)}'
before the -lt comparison. Matches Stage 2 pwsh's
[int][math]::Floor([double]$Matches[2]) semantically.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Stage 2 (Windows) coverage gate regex
^\s*(\S+)\s+.*(\d+(?:\.\d+)?)%\s*$
had a greedy `.*` between the module name and the trailing `\d+%`,
which ate all but the last digit of the percent. On lines like
Wolfgang.Extensions.<X> 100%
the regex captured percent=0 (the last "0" of "100"), reported the
module as failing the 90% threshold, and tanked the entire gate even
on 100%-covered code. First surfaced on DateTime-Extensions vNext —
PR #189 has the original fix.
Two changes to align with Stage 1 (Linux), whose awk-based parser is
correct:
- Anchor on `^(\S+)` to skip indented sub-class rows (Stage 1's
`^[^ ]` does the same — assembly rows carry the aggregate
percent, so nothing is lost).
- Drop the `.*`; let `\s+` separate the module from the final
`\d+%` directly, so there is no greedy region to swallow digits.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
--- updated-dependencies: - dependency-name: coverlet.collector dependency-version: 10.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
--- updated-dependencies: - dependency-name: Meziantou.Analyzer dependency-version: 3.0.93 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <support@github.com>
--- updated-dependencies: - dependency-name: SonarAnalyzer.CSharp dependency-version: 10.26.0.140279 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <support@github.com>
Triage summary on the Copilot review threadsVerified each concern against the actual state on Stale —
|
The workflow consumes coverlet.runsettings via dotnet test --settings coverlet.runsettings in all three test stages (Linux/Windows/macOS), but the file was not in any of the trusted-config protection lists: 1. detect-projects exact_files (guard) — a PR could modify coverlet.runsettings without triggering the maintainer-review gate. 2. The four fetch-from-main lists (one per test/scan job) — under pull_request_target, the workflow YAML is trusted but the checkout uses the PR head. Without fetching coverlet.runsettings from main, the PR-controlled version is what dotnet test reads. A PR could add <Exclude>**/*</Exclude> to coverlet.runsettings to exclude all source files from coverage, then everything that does run through the parser shows 100% coverage (or the matched-modules guard fires, depending on parser layout). Either way the coverage gate is weakened. Adding coverlet.runsettings to all 6 lists (5 fetch + 1 exact_files guard) closes the gap. The file is present in this repo and will be fetched from main in every CI job; PRs that modify it will now also trigger the maintainer-review path. Surfaced by Copilot review on PR #135. Fleet-wide canonical workflow fix — the next template-sync wave fans this out to the other 25 repos. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triage on the 7 newer unresolved threads (page-2 catch)Stray YAML block claim (1 thread, 2026-05-25)Stale. The workflow has been running on every PR (it parses fine). Examined the file end-to-end:
|
Template-drift resolution for
IEnumerable-Extensions(initiative C1) plus eight additional canonical initiatives stacked onto this samecanonical-protectedbranch over the C1 campaign. Review carefully before admin-bypass merge — bypass waives all ruleset gates.Stacked scope (in commit order)
pr.yamlDetect .NET Projects guard).<Nullable>enable</Nullable>hoisted into rootDirectory.Build.props(conditioned to.csprojonly); per-csproj<Nullable>lines removed for atomicity (this is why the PR also touches src/tests/examples csproj files — they must merge together with theDirectory.Build.propschange so Release builds never see a window with nullable annotations but no nullable context).queries: security-extendedadded to.github/workflows/codeql.yaml.verify-docs-buildjob added to.github/workflows/release.yaml;publish-nugetnow needs[pack-and-validate, verify-docs-build]so a docs failure blocks the release.versions.jsonpreservation guard added to.github/workflows/docfx.yaml(aborts deploy if previously-published version entries would be lost).Microsoft.CodeAnalysis.PublicApiAnalyzersinfrastructure added toDirectory.Build.propswith opt-inAdditionalFilesglobbing (per-project enablement viaPublicAPI.Shipped.txt/PublicAPI.Unshipped.txt).Authors/Company/Copyright/RepositoryType/SymbolPackageFormat/SourceLink/symbols) added to rootDirectory.Build.props.docfx.yaml(publishes/coverage/alongside/api/on the docs site)..github/workflows/stryker.yaml) folded in via merge commit (was previously a separatet3-stryker-mutation-testingPR; consolidating onto this branch reduces the per-repo release flow to one admin-bypass merge instead of two).Why these ride together
The
pr.yaml"Detect .NET Projects" guard fires on protected file changes and forces admin-bypass merge. Bypass waives every ruleset rule at once, so each canonical pass costs at most one bypass per repo by design.Per the C1 plan's standing-branch model, additional canonical changes stack onto this branch rather than fanning out as separate admin-bypass PRs.
🤖 Generated with Claude Code
Closes
This PR lands the protected canonical baseline via admin-bypass. When it merges, GitHub auto-closes the following Tier-1 maintenance sub-issues that were addressed by the protected drift / S1 / D6 / D8 / A1 / CI3 / T1 / T3 content folded into
canonical-protected:The unprotected / feature work + the rest of the Maintenance board sub-issues close via PR #153 (
vNext → main) which merges after this PR.