Skip to content

Canonical-protected: C1 drift + nullable + S1 + D6 + D8 + A1 + CI3 + T1 + T3#184

Closed
Chris-Wolfgang wants to merge 66 commits into
mainfrom
canonical-protected
Closed

Canonical-protected: C1 drift + nullable + S1 + D6 + D8 + A1 + CI3 + T1 + T3#184
Chris-Wolfgang wants to merge 66 commits into
mainfrom
canonical-protected

Conversation

@Chris-Wolfgang

@Chris-Wolfgang Chris-Wolfgang commented May 22, 2026

Copy link
Copy Markdown
Owner

Template-drift resolution for ETL-Abstractions (initiative C1) plus eight additional canonical initiatives stacked onto this same canonical-protected branch over the C1 campaign. Review carefully before admin-bypass merge — bypass waives all ruleset gates.

Stacked scope (in commit order)

  1. C1 — protected-file drift re-sync (root config / workflow files that trip the pr.yaml Detect .NET Projects guard).
  2. Nullable consolidation<Nullable>enable</Nullable> hoisted into root Directory.Build.props (conditioned to .csproj only); per-csproj <Nullable> lines removed for atomicity (this is why the PR also touches src/tests/examples csproj files — they must merge together with the Directory.Build.props change so Release builds never see a window with nullable annotations but no nullable context).
  3. S1 — CodeQL queries: security-extended added to .github/workflows/codeql.yaml.
  4. D8verify-docs-build job added to .github/workflows/release.yaml; publish-nuget now needs [pack-and-validate, verify-docs-build] so a docs failure blocks the release.
  5. D6versions.json preservation guard added to .github/workflows/docfx.yaml (aborts deploy if previously-published version entries would be lost).
  6. A1Microsoft.CodeAnalysis.PublicApiAnalyzers infrastructure added to Directory.Build.props with opt-in AdditionalFiles globbing (per-project enablement via PublicAPI.Shipped.txt/PublicAPI.Unshipped.txt).
  7. CI3 — Fleet-canonical NuGet metadata defaults (Authors/Company/Copyright/RepositoryType/SymbolPackageFormat/SourceLink/symbols) added to root Directory.Build.props.
  8. T1 — Coverage-report generation step added to docfx.yaml (publishes /coverage/ alongside /api/ on the docs site).
  9. T3 — Stryker mutation-testing workflow (.github/workflows/stryker.yaml) folded in via merge commit (was previously a separate t3-stryker-mutation-testing PR; 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

Template-drift resolution for ETL-Abstractions — the protected half. 5 files
that trip the pr.yaml guard, isolated for admin-bypass merge.

Part of #155.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings May 22, 2026 15:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Resyncs a set of protected, template-tracked configuration and GitHub workflow files as part of the C1 template-drift initiative (Issue #155), including CI hardening and documentation deployment behavior.

Changes:

  • Updates pr.yaml to add dotnet workload restore across CI stages and adjusts how “trusted config files from main” are fetched in the Windows stage.
  • Updates docfx.yaml SemVer prerelease ordering logic and changes gh-pages root cleanup behavior during deployment.
  • Refreshes protected repo configuration text (banner/editorconfig commentary).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
BannedSymbols.txt Updates banner text and trims trailing content.
.github/workflows/pr.yaml Adds .NET workload restore and refactors trusted-config fetch behavior in parts of the pipeline.
.github/workflows/docfx.yaml Adjusts SemVer prerelease comparator and modifies gh-pages cleanup/deploy flow (incl. preserving dev/).
.github/workflows/codeql.yaml Adds a workload-restore step before building for CodeQL.
.editorconfig Refines comments describing PowerShell encoding/line-ending expectations.

Comment thread BannedSymbols.txt Outdated
@@ -1,4 +1,4 @@
# BannedSymbols.txt - Async-First Enforcement for Wolfgang.Etl.Abstractions
# BannedSymbols.txt - Async-First Enforcement for {{PROJECT_NAME}}
Comment thread .github/workflows/docfx.yaml Outdated
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>
@Chris-Wolfgang

Copy link
Copy Markdown
Owner Author

Scope added to this PR — nullable consolidation. <Nullable>enable</Nullable> is now set once in the root Directory.Build.props, and the redundant per-csproj <Nullable> lines have been removed from every SDK-style project. Both halves are on this protected branch so they merge atomically — there is never a window where nullable reference types are off. Legacy non-SDK example projects do not import Directory.Build.props and keep their explicit settings.

Chris-Wolfgang and others added 4 commits May 22, 2026 15:55
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>
@Chris-Wolfgang

Copy link
Copy Markdown
Owner Author

Scope additions to this PR — S1 + D8 stacked.

The canonical-protected branch now also carries:

  • S1queries: security-extended added to .github/workflows/codeql.yaml (3-line addition under the CodeQL init step) for broader security-query coverage.
  • D8 — new verify-docs-build job in .github/workflows/release.yaml; publish-nuget now needs: [pack-and-validate, verify-docs-build] so a docs failure blocks the release before the NuGet package goes live.

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>
@Chris-Wolfgang

Copy link
Copy Markdown
Owner Author

Scope addition to this PR — D6 stacked.

Adds a Verify previous versions preserved step to .github/workflows/docfx.yaml (right before Compute destination directory). The step fetches the currently-deployed versions.json from gh-pages and aborts the deploy if the newly-generated file would shrink or drop any previously-published version label — guards against accidentally wiping the docs version selector.

Chris-Wolfgang and others added 3 commits May 23, 2026 14:43
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>
@Chris-Wolfgang

Copy link
Copy Markdown
Owner Author

Scope additions to this PR — A1 + CI3 + T1 stacked.

  • A1Microsoft.CodeAnalysis.PublicApiAnalyzers added to root Directory.Build.props with opt-in AdditionalFiles globbing (Exists() conditions on PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt). Infrastructure only — analyzer stays dormant per-project until the txt files are dropped into a src project dir. Per-repo enablement is tracked separately.
  • CI3 — Fleet-canonical NuGet metadata defaults (Authors / Company / Copyright / RepositoryType / SymbolPackageFormat / SourceLink + symbols). Repo-specific NuGet fields (Description, Tags, ProjectUrl, RepositoryUrl, LicenseExpression, PackageReadmeFile) remain in each src csproj and are tracked as per-repo follow-up.
  • T1.github/workflows/docfx.yaml gains a "Generate code-coverage report" step that runs the test suite with Cobertura coverage collection and publishes a ReportGenerator HTML report under /coverage/ on the docs site. continue-on-error: true keeps a coverage failure from blocking the docs deploy.

Each canonical-protected PR now carries: C1 protected drift + nullable consolidation + S1 + D6 + D8 + A1 + CI3 + T1.

Chris-Wolfgang and others added 3 commits May 23, 2026 17:55
- BannedSymbols.txt: replace {{PROJECT_NAME}} placeholder with the repo's
  package name (skipped on repo-template where the placeholder is the
  intended template artifact).
- docfx.yaml: 'exit 1' inside the deploy try-block changed to 'throw' so
  the outer finally always unsets the global http.extraheader token; added
  $LASTEXITCODE checks after git fetch / git worktree add / git init /
  git remote add so a setup failure surfaces a clear error.

Fan-out of the round-2 Copilot fixes verified against DateTime-Extensions
(#178 / #179 pilot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- .github/workflows/stryker.yaml: replace literal-in-array config detection
  with explicit [ -f ] checks. nullglob only drops words that look like
  globs (contain *, ?, [); the bare literal 'stryker-config.json' was
  preserved unconditionally, so the workflow would mark found=true and
  attempt to install Stryker even on repos with no config.
- actions/checkout@v4 -> @v6 and setup-dotnet@v4 -> @v5 for consistency
  with the rest of the fleet's workflows.

Fan-out of the round-2 Copilot fixes verified against DateTime-Extensions
(#181 pilot).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stryker.yaml lives under .github/workflows/*, which means it trips the
pr.yaml "Detect .NET Projects" guard the same way the rest of
canonical-protected does. Keeping T3 on its own branch would have meant
two admin-bypass merges per repo (canonical-protected -> main, then
t3-stryker -> main). Folding T3 in here collapses that to one bypass on
the way to release.

Merges origin/t3-stryker-mutation-testing into canonical-protected. The
existing per-repo T3 PR will be closed as superseded once this lands.
@Chris-Wolfgang Chris-Wolfgang changed the title C1 drift: re-sync protected config + workflow files Canonical-protected: C1 drift + nullable + S1 + D6 + D8 + A1 + CI3 + T1 + T3 May 23, 2026
@Chris-Wolfgang

Copy link
Copy Markdown
Owner Author

Scope addition to this PR — T3 (Stryker) folded in.

The Stryker mutation-testing workflow (.github/workflows/stryker.yaml) was previously on its own per-repo t3-stryker-mutation-testing branch with a separate PR. Because stryker.yaml lives at a protected path (.github/workflows/*), merging it as a separate PR would have required a second admin-bypass on the way to release.

Folded the T3 branch into canonical-protected via merge commit so the per-repo release flow needs only one admin-bypass merge (canonical-protected → main). The corresponding t3-stryker-mutation-testing PR for this repo is being closed as superseded.

Per-repo Stryker enablement (dropping a stryker-config.json into the test project) is still tracked as a follow-up — this PR only ships the workflow infrastructure.

Chris-Wolfgang and others added 7 commits May 24, 2026 10:14
Add '$(UsingMicrosoftNETSdk)' == 'true' to the existing .csproj-only
condition so the property only applies to SDK-style projects. Legacy
non-SDK csproj files no longer pick up <Nullable>enable</Nullable> by
inheritance; they would have to opt in explicitly.

Verified safe by piloting on IComparable-Extensions — full Release
build + 540-test run (54 tests x 10 TFMs) passed cleanly with the
tightened condition. Resolves the PR-#390 review thread that asked for
this addition.

The per-repo explicit <Nullable>disable</Nullable> opt-outs already in
place on Try-Pattern (examples/CSharp.DotNet462.Example) and D20-Dice
(examples/Net4.8/Example1-Console) become redundant but harmless under
this condition — they can be cleaned up later, or left as belt-and-
suspenders documentation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Brings the canonical Directory.Build.props in line with repo-template and
D20-Dice, which carry an explanatory comment above the <Nullable> line
describing what the .csproj + $(UsingMicrosoftNETSdk) condition excludes
and where the remaining opt-out path lives.

No behavior change — comment only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
T3's stryker.yaml was cherry-picked carrying actions/upload-artifact@v4,
while pr.yaml/release.yaml/codeql.yaml all use @v7. Bumping for
consistency with the fleet's canonical action versions.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The flag is a Windows-PowerShell-5.1-only switch; pwsh (PowerShell 7+)
treats it as unsupported and errors. The step runs under shell: pwsh, so
the call must omit it. (Already correct in the alternate path elsewhere
in this workflow — bringing this one in line.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`dotnet tool install -g` errors with a non-zero exit code if the tool is
already installed (common on self-hosted runners and after prior steps).
Even with stderr redirected, the exit code can break subsequent invocations.

Switch to update-or-install: try update first (succeeds if installed),
fall back to install if not. The step runs under shell: pwsh so the
pwsh 7 || operator is available.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Get-ChildItem -Filter '*.sln' missed .slnx solutions, causing the
restore/build warm-up to silently skip in repos using the newer
solution format (e.g. IComparable-Extensions). Switching to a filter
that accepts both extensions so DocFX gets a compiled solution to
extract metadata from.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
build-all-versions.yaml built versions.json from every SemVer tag in the
repo, regardless of whether the per-tag build succeeded. A failed worktree
add or empty DocFX output would silently leave the version-picker linking
to /versions/<tag>/ paths that never existed on gh-pages.

Now filter $orderedTags against the directories actually present under
$outDir/versions/. Missing tags get a ::notice:: log entry so the skip is
visible in workflow output, but versions.json only references real paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chris-Wolfgang and others added 24 commits May 24, 2026 23:31
CI3 centralized Authors/Company/Copyright in Directory.Build.props:

    <Authors>Chris Wolfgang</Authors>
    <Company>Chris Wolfgang</Company>
    <Copyright>Copyright (c) Chris Wolfgang</Copyright>

But the same three properties remained set to identical values in the
per-project csprojs that pre-dated CI3, so each csproj was effectively
shadowing the inherited default with the same value. That works
today, but two failure modes:

  - If we ever change the canonical defaults, csprojs that still set
    their own override would silently keep the old value.
  - The duplication is dead-weight maintenance — one rename in
    Directory.Build.props wouldn't propagate to every src csproj.

Stripping the three canonical-default properties only when their
csproj value exactly matches the centralized one. Per-csproj fields
that vary (Description, PackageTags, PackageProjectUrl, RepositoryUrl,
PackageLicenseExpression, PackageReadmeFile, Version) are untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The step comment said "Fetches the currently-deployed versions.json
from gh-pages", but the implementation fetches it from the published
GitHub Pages URL via Invoke-WebRequest, not from the gh-pages branch
directly. The two are usually equivalent but can diverge if Pages
hasn't redeployed yet, custom domains are involved, or Pages is
disabled. Be precise in the comment so the actual behavior is clear.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
An earlier commit dropped `windows` from the workload-bearing TFM
detection regex (netX.Y-windows TFMs are WPF/WinForms/WindowsDesktop,
which use SDK-bundled projection assemblies and don't need the .NET
workload installer). The accompanying comment still listed `windows`
in the bracket-enumeration, which contradicted the regex below.

Update the comment to match — `browser` is now the last entry, and a
short trailing note explains why `-windows` is intentionally absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The coverage step ran dotnet test without --framework, so the docs
deploy job re-ran the entire multi-TFM test suite every time. That's
redundant — pr.yaml already exercises every TFM across Stages 1/2/3,
and the coverage report only needs one TFM's worth of runs.

Pinning to net10.0 (the modern target always present in this fleet):
  - Cuts docfx job time substantially
  - Removes failure surface from older targets (transient quirks that
    wouldn't block a release but would block the docs deploy)
  - Keeps the published coverage report focused on the current target

Verified locally on DateTime-Extensions: dotnet build -c Release clean,
dotnet test --framework net10.0 → 66/66 tests passed, cobertura
generated. Fanning out to the rest of the fleet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "gracefully skip when no tests" branches in Stages 1/2/3 let a PR
remove the entire ./tests directory (or all test projects within it)
and still pass CI — even when CODECOV_MINIMUM=90 nominally enforces a
coverage gate downstream. Copilot flagged this as a coverage-gate
bypass.

New rule:
  - If ./src contains *.csproj/*.vbproj/*.fsproj, missing/empty
    ./tests fails the stage with a clear error. Real source code
    must be covered by real tests.
  - If ./src is also empty (template-pack repos, in-dev repos with
    no source yet), the stage still skips gracefully. That preserves
    the carve-out the original "graceful skip" was added for.

Applied to all four test-discovery blocks: Stage 1 Linux bash,
Stage 2 Windows pwsh, Stage 3 macOS bash, and the macOS-ARM64 bash
variant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier "Stage 2 coverage parse: accept extra columns + fail on
zero matches" commit corrupted pr.yaml on every canonical-protected
branch. The JS replace's replacement string contained a literal `$\'`
inside the regex, which String.prototype.replace interprets as the
"right-context" replacement token — so it inserted the rest of the
file inline after the regex line. The result was a YAML with
duplicate `security-scan:` jobs and an unterminated regex string,
which would have failed to parse at next workflow run.

Recovery:
  1. Restore pr.yaml from the parent of the broken commit (last
     known-good state).
  2. Re-apply the three post-broken-commit changes that legitimately
     touched pr.yaml: --no-build --no-restore on dotnet test, header
     bullet dedup, and tests-gate strict mode (fail when src has
     projects but tests don't).
  3. Re-apply the Stage 2 coverage regex fix correctly, using a
     replacer function instead of a literal NEW string so `$\'` and
     other `$` tokens are not interpolated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Write-Host isn't a pipeline sink — piping a native command's combined
stdout/stderr to it (`dotnet restore ... 2>&1 | Write-Host`) can
produce parameter-binding errors and interfere with surfacing the
command's actual output. Out-Host is the correct sink: it writes to
the host stream just like Write-Host but properly accepts pipeline
input.

Replaced 4 occurrences in the per-tag build try-block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Policy: TreatWarningsAsErrors=true applies to ALL projects in Release
(src + tests + examples + benchmarks). The subtree Directory.Build.props
files in tests/, examples/ were overriding the root
property back to false for their respective subtrees, contradicting the
stated convention.

Per-csproj <NoWarn> is the right tool for stylistic / test-pattern /
benchmark-specific suppressions — broad TWEA flips at the subtree level
are too coarse and let real regressions slip through CI. (The
.editorconfig [tests/**/*.cs] relaxations don't help either: the Windows
runner doesn't honor them, so test-project warnings need per-csproj
<NoWarn> regardless.)

Each removed file contained only the MSBuild Import + the TWEA override,
nothing else worth preserving. After this commit, tests/, examples/ subtree(s)
inherit root TWEA=true in Release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier "Fail job when protected-config fetch/copy fails" commit
fixed the visible silent-fallback bug, but introduced a subtler one:
the `while read` loop was fed by a pipeline (`git ls-tree | grep | while`),
so it ran in a subshell. `exit 1` from inside the loop terminated only
the subshell — the outer step continued, defeating the very guarantee
I'd just added.

Switched to process substitution (`done < <(git ls-tree ... | grep ...)`).
The loop now runs in the parent shell; `exit 1` reliably aborts the
step on a `git show` failure, preserving the trusted-main contract.

Applied to all three stage copies of the pattern (Stages 1/2/3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier "accept extra columns" fix used `.*?` (non-greedy)
between the module name and the percent capture group. That matches
the FIRST `%` on the line, not the last as the comment claimed.

ReportGenerator Summary.txt rows look like:

    Foo.csproj   85.2%   92.1%

With the non-greedy regex, $Matches[2] captured 85.2 (line coverage)
instead of 92.1 (the overall figure we wanted to gate on).

Switching to greedy `.*` so the regex engine consumes as much as
possible before backtracking to satisfy the trailing `(\d+...)%\s*$`.
That captures the last percent before end-of-line.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Stage 2 coverage regex: use greedy match to capture LAST percent"
commit re-corrupted pr.yaml the same way the original "accept extra
columns" commit did: the JS replacement string contained `$'` inside
the regex literal, which String.prototype.replace interprets as the
right-context replacement token — splicing half the file inline.

Restored pr.yaml from the parent of the broken commit (the
"run protected-config copy loop in parent shell" commit, which was
known-good) and re-applied the .*? -> .* change using a REPLACER
FUNCTION instead of a literal replacement string. The replacer
function form (`body.replace(needle, () => replacement)`) does NOT
interpret $ tokens in the replacement.

Note for future: any time the NEW string in a body.replace contains
$' / $` / $& / $N etc., either use a replacer function or escape
each $ as $$. Bitten by this twice now.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The earlier CI3 dedupe only matched the exact canonical value
("Copyright (c) Chris Wolfgang"), so year-specific variants like
"Copyright 2026 Chris Wolfgang" (sometimes with trailing whitespace)
slipped through and continued to shadow the centralized default.

Stripping any csproj <Copyright> line that matches
"Copyright YYYY Chris Wolfgang" so the canonical year-less value in
Directory.Build.props flows through. Per-csproj fields that legitimately
vary (Description, PackageTags, PackageProjectUrl, RepositoryUrl,
PackageLicenseExpression, PackageReadmeFile, Version) remain untouched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sing

The guard runs only when deploy_to_pages != false (real deploy), and
DEPLOY_AS_LATEST=true wipes the gh-pages root before depositing the new
content. If `docfx_project/_site/versions.json` doesn't exist when we
reach this step, the docfx build is broken — and silently `exit 0`-ing
let the downstream deploy proceed and overwrite the root with whatever
state (possibly losing previously-published versions in the picker).

Failing the step instead. A missing versions.json now blocks the
deploy with a clear ::error:: pointing at docfx generation, which is
safer than allowing a partial / unverified publish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Directory.Build.props sets IncludeSymbols=true + SymbolPackageFormat=snupkg
(CI3), so dotnet pack produces .snupkg symbol packages next to each
.nupkg. The attach-to-release step globbed only *.nupkg, leaving symbol
packages off the GitHub Release page.

Adding *.snupkg to the file list so users who download artifacts from
the Release can grab matching symbol packages too. Publishing to
nuget.org is unaffected — `dotnet nuget push *.nupkg` already auto-
detects and uploads adjacent .snupkg files to NuGet's symbol server.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related bugs in the single-commit-and-push block:

  1. `git diff --cached --quiet` returns 0 (no changes), 1 (changes
     exist), or >1 (error reading the staged index). The current
     `if ($LASTEXITCODE -ne 0)` treated >1 (error) the same as 1
     (changes), so an index-read failure would route into the
     commit/push branch and try to commit anyway.

  2. After `git commit`, the script unconditionally ran `git push`
     with no $LASTEXITCODE check. A commit failure (config/auth/empty
     commit) would let the push proceed against a stale HEAD, and
     $deployExitCode at the end of the try block reflected only the
     push's exit code, masking the commit failure.

Now: explicit numeric branches on diff exit, abort on >1, and
$LASTEXITCODE checks between commit and push that throw on failure
(so the finally block's cleanup still runs). The deploy now fails
loudly and visibly when any step in the chain errors.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Run step had an if/else that ran EITHER stryker-config.json at repo
root OR any tests/**/stryker-config.json — never both. But the Detect
step's "found" output is true when EITHER (or both) exist, so a repo
with a root umbrella config plus per-suite configs would only get the
umbrella scanned; per-suite configs were silently skipped.

Now both branches run independently. Added a "ran=0/1" sentinel that
errors out if neither shape was actually present (defensive guard
against Detect/Run drift).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage 2 (Windows) coverage parser was hardened earlier — greedy regex
+ matched-count guard that fails when no modules parse. Stage 1
(Linux bash) still ran the original loose awk parser without the
guard, so if Summary.txt format changes (or the report is malformed
in a way that bypasses the line regex), Stage 1 would silently pass
with an empty failed_projects list — exact opposite of Stage 2's
new behavior.

Adding the same matched_count guard so Stage 1 matches Stage 2
semantically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1. (Stage 1/3 bash, exact-file branch) Added an explicit exit-code
   check around `git show > "$config_file"`. The globbed branch was
   hardened earlier; the exact-file branch had been left silently
   falling back to the PR version on any git show failure.

2. (Stage 1/3 bash, globbed branch) The process substitution
   `done < <(... | grep -E ...)` runs under `set -eo pipefail`, and
   grep exits 1 when no entries match. For repos that don't ship a
   `*.ruleset` (or any other optional pattern), the previously-fine
   "no matches → skip" behavior was actually failing the step.
   Wrapped the grep in `{ ... || true; }` so an empty match is
   treated as zero iterations, not a failure.

3. (Stage 2 pwsh) `Out-File ... -NoNewline` strips trailing newlines
   from the copied workflow/globalconfig/ruleset/editorconfig files,
   producing malformed copies. Removed -NoNewline so the file's
   trailing newline is preserved.

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>
Chris-Wolfgang added a commit that referenced this pull request May 26, 2026
Every shippable src csproj across the fleet had `<AssemblyVersion>1.0.0</AssemblyVersion>`
hardcoded — last touched at v1.0.0 and never bumped. Released NuGet
packages from v1.1.0 onward have shipped with AssemblyVersion=1.0.0
even though the package <Version> bumped normally.

Drop the explicit AssemblyVersion line. .NET SDK derives AssemblyVersion,
FileVersion, and InformationalVersion from <Version> when they are not
specified, so all four track every <Version> bump automatically going
forward. They are not byte-identical — AssemblyVersion is normalized to
a 4-part System.Version, InformationalVersion can carry a +<git-sha>
under SourceLink — but they all reflect the current Version, which is
the point.

Mirrors DateTime-Extensions PR #184 (already merged to main).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Chris-Wolfgang

Copy link
Copy Markdown
Owner Author

Superseded by PR #192 (vNext → main release). Content is folded into the vNext release PR alongside the rest of the canonical maintenance round.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants