Skip to content

release: 0.17.0 - #320

Merged
Chris-Wolfgang merged 39 commits into
mainfrom
release/prep-0.17.0
Jul 24, 2026
Merged

release: 0.17.0#320
Chris-Wolfgang merged 39 commits into
mainfrom
release/prep-0.17.0

Conversation

@Chris-Wolfgang

Copy link
Copy Markdown
Owner

Release 0.17.0

Merges vNext into main with the version bump. MINOR — the release introduces one behavioural change (use-after-dispose now throws); no public signatures were added, removed, or altered, so Package Validation passes against the 0.16.1 baseline and no binding redirect is needed (AssemblyVersion stays 1.0.0.0).

Version

  • <Version> 0.16.1 → 0.17.0
  • <PackageValidationBaselineVersion> 0.16.0 → 0.16.1 (last published)

The behavioural change

Using a disposed component now throws ObjectDisposedException. Every public entry point on ExtractorBase / LoaderBase / TransformerBase (all ExtractAsync / LoadAsync / TransformAsync overloads) rejects calls made after Dispose()/DisposeAsync() instead of silently running. Code that reused a component after disposing it previously "worked" and will now throw — construct a new instance per run. Dispose remains idempotent.

What else is in it (PRs #307#319)

Note on the protected-file guard

This diff touches 5 workflows plus .github/sourcelink/consumer/Directory.Build.*, so Detect .NET Projects will fail here by design. Same as the 0.16.1 cycle, that needs a separate protected-files PR merged to main first (admin bypass), after which merging main back into this branch clears the guard and this PR gets full CI.

Closes

Closes #205, closes #207, closes #212, closes #214, closes #217, closes #225, closes #250.
(#208 stays open — package signing deferred.)

Chris-Wolfgang and others added 30 commits July 22, 2026 13:19
The test asserted an exact callback-count equality after only a fixed 50ms
grace following StopTimer(). A tick already dispatched when StopTimer was called
(at most one) can land after that grace on a slow/loaded runner, incrementing
the count and failing the exact-equality assertion.

Replaces the fixed grace with a WaitUntilStable poll that returns once the count
has been unchanged for a full window — so the last in-flight tick is absorbed
regardless of runner speed — then still asserts the count stays put across
several further intervals (a running timer would fire ~4 more). Bounded by an
overall deadline so a genuinely-broken StopTimer fails the assertion instead of
hanging.

Verified: 5/5 green in Release.

Closes #250

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests/…Tests.Unit/Performance/AllocationFreeTests.cs. Some members are read
or invoked once per record / progress tick and must not allocate — a change that
"looks identical but allocates" silently regresses throughput and GC pressure
for high-volume consumers. Each test measures managed allocation on the current
thread across a tight loop (GC.GetAllocatedBytesForCurrentThread, available on
every TFM net462+/netcoreapp3.1+) and asserts it rounds to zero per call.

Documented allocation-free allowlist (asserted): Report.CurrentItemCount /
ItemsPerSecond / PercentComplete (double?) / EstimatedRemaining (TimeSpan?), and
ExtractorBase.IncrementCurrentItemCount / IncrementCurrentSkippedItemCount
(Interlocked). The per-call threshold is a sub-byte epsilon to absorb one-time
tiered-JIT recompilation amortised across the loop; a real per-call allocation
(boxing, closure) is >=24 bytes and fails cleanly with the actual byte count.

Verified: 6/6 green on net10.0 and net48.

Closes #217

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds an actions/attest-build-provenance step to the release's
update-release-artifacts job: each packed .nupkg gets a signed, verifiable SLSA
provenance statement (built by this repo's release.yaml, from a specific commit),
stored in the repo's attestation store. Grants the job id-token:write +
attestations:write for it. Complements the CycloneDX SBOM already shipped.

SECURITY.md gains a "Verifying the supply chain" section documenting how a
consumer verifies the SBOM + attestation
(`gh attestation verify <pkg> --repo Chris-Wolfgang/ETL-Abstractions`), and
explicitly flags that Authenticode/NuGet package signing is NOT yet enabled.

Partial for #208: SBOM (already) + SLSA attestation are done; package signing
(`nuget verify`-able signatures) still needs a code-signing certificate and
stays open on #208.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Turns the existing (report-only) Stryker setup into an enforced release-gate
quality bar. stryker-config.json `break` goes 0 -> 70: dotnet stryker now exits
non-zero (failing the job) if the mutation score drops below the floor. The floor
sits a few points under the measured full-project baseline (74.4% on Stryker
4.16.0, 2026-07-22) so CI-runner timeout variance doesn't flake it, while a real
test-suite regression trips it.

stryker.yaml gains a path-scoped pull_request trigger (src/** changes) so a PR
that would drop the score below the floor is caught BEFORE merge — scoped to
source because a full run takes tens of minutes; docs/test-only PRs skip it. The
Stryker install is pinned to 4.16.0 so the gate is reproducible. (Note: this repo
is on BannedApi 4.14 / PublicApi 3.3.4, not the 5.6 analyzers, so it is unaffected
by the Stryker/CodeAnalysis-5.6 crash that blocks some fleet repos.)

docs/mutation-testing.md documents the baseline, the floor, the ratchet-up-only
policy, and how to run locally.

Partial for #205: the enforced floor + PR gate (the load-bearing "release-gate
quality bar") are done. The score-history chart and auto-filing of
kind:mutation-survives issues are deferred; reports upload as a workflow artifact
meanwhile.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes #205's remaining acceptance criteria on top of the enforced floor:

- Score history: non-PR (schedule/dispatch) runs parse the final mutation score
  from the Stryker log and publish it to gh-pages /dev/stryker via
  github-action-benchmark (customBiggerIsBetter — same action/pin as the BDN
  charts), one data point per run. Trend visibility only; fail-on-alert:false so
  it never double-fails the run (the `break` floor is the gate).
- Survivor tracking: non-PR runs write the survivor worklist (file:line + mutator,
  from mutation-report.json) to the job summary and keep ONE rolling
  kind:mutation-survives issue up to date — deliberately not one issue per
  survivor (dozens today = noise). Best-effort; never fails the run.

The Run step now tees Stryker output with `set -o pipefail` so the gate's
non-zero exit is preserved through the pipe. Publish/track steps run only on
non-PR events, so the PR gate path is unchanged.

Closes #205

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Flips the reproducible-build story to the consumer side: #216 proves our build is
reproducible internally; this documents how a third party verifies it
independently.

- docs/REPRODUCIBLE-BUILD.md — the exact tooling (SDK 10.0.x,
  ContinuousIntegrationBuild=true), the rebuild command, how to hash the shipped
  assembly (the deterministic artifact — NOT the .nupkg envelope, whose zip
  timestamps are non-deterministic), how to compare against the release manifest,
  how to file a discrepancy, and the third-party attestation conventions
  (reproducible-builds.org / vouchsafe.io / attest-build-provenance over a rebuild).
- release.yaml now generates reproducible-build-manifest.json — the expected
  sha256 of every shipped TFM assembly, extracted from the .nupkg — and attaches
  it to the GitHub Release. Verified the extraction/hash logic against the live
  0.16.1 package (all 11 TFM assemblies).
- README gains a "Verify the build" section linking the procedure.

Closes #225

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds tests/Wolfgang.Etl.Abstractions.Tests.Concurrency + .github/workflows/coyote.yaml.
Microsoft Coyote rewrites the assemblies to control the task scheduler and replays
each test under thousands of distinct interleavings, surfacing races/deadlocks a
normal single-schedule run never hits. `coyote test` exits non-zero on a found bug,
so it is an enforced gate.

Two systematic tests over the base-class concurrency:
- concurrent per-item counter increments never lose an update (the counter is
  Interlocked — a non-atomic ++ would surface as a lost update on some schedule);
- DisposeAsync racing an in-flight enumeration never deadlocks.

Both explore ~1600 unique execution paths locally with 0 bugs found.

The project targets net8.0 because the Coyote 1.7.x CLI cannot load net10.0
assemblies (it predates that runtime); the concurrency explored is
runtime-agnostic. coyote.yaml gates PRs touching src/** (1000 iterations/test) and
runs the deep exploration weekly + on dispatch (10000 iterations/test).

The optional 24h soak (leak hunting via dotnet-counters) is NOT implemented — it
needs a persistent self-hosted runner this project lacks; documented in the
workflow header for when one exists.

Closes #207

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Proves the debugger half of SourceLink end-to-end: sourcelink.yaml already checks
that every PDB document resolves to real GitHub content; this drives an actual
debugger step-into and asserts it lands in the library's real source, not a
decompiled placeholder.

.github/workflows/sourcelink-stepinto.yaml (scheduled weekly + workflow_dispatch,
not a PR gate) builds a fixture consumer, installs netcoredbg, and runs
verify_stepinto.py — which drives netcoredbg over its MI interface to break in the
consumer, step into Report's constructor, and assert the frame is the library's
SourceLink-mapped source (/_/src/Wolfgang.Etl.Abstractions/Report.cs) with symbols
loaded.

Uses a ProjectReference built with ContinuousIntegrationBuild=true (byte-identical
SourceLink PDB to the shipped package; netcoredbg can't pair a package assembly
with its .snupkg PDB) and Debug/Optimize=false so the target isn't inlined. An
empty Directory.Build.props/.targets isolates the fixture from repo analyzers.

Validated locally end to end (netcoredbg 3.1.2-1054, net10.0):
step-into resolved /_/src/Wolfgang.Etl.Abstractions/Report.cs, symbols-loaded=1,
RESULT=PASS.

Closes #214

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test: de-flake StopTimer_prevents_further_callbacks (#250)
Adds a scheduled GC / allocation profile — the sustained-load metrics BDN
micro-benchmarks don't capture (gen0/1/2 promotion, LOH pressure, finalizer-queue
depth, thread-pool starvation under continuous ETL traffic).

- tools/GcProfileWorkload — a workload that runs an in-memory extract -> transform
  -> load Pipeline (ExtractorBase -> TransformerBase -> LoaderBase, per-stage
  progress + stage disposal) in a loop for a configurable duration, under ServerGC.
- .github/workflows/gc-profile.yaml (weekly Sunday 07:00 UTC + workflow_dispatch)
  runs the workload for 10 minutes, attaches dotnet-counters by PID to sample the
  System.Runtime counter set to a CSV, summarises into the step summary, and
  uploads reports/ as an artifact.
- docs/GC-PROFILE.md documents what runs, how to read the report, and the ratchet
  policy.

Gate mode: informational (no regression gate yet — that needs a stable baseline
the first several runs establish; same land-informational-harden-later pattern as
reproducible-build/semgrep/stryker). Actions SHA-pinned per the repo convention.

Validated locally: the workload builds and runs — ~90M records in 8s with only
2 gen0 collections, 0 gen1/gen2, ~6MB allocated (the streaming pipeline is
near-zero-alloc per record, consistent with #217). The regression gate is the
documented follow-on once baseline runs accumulate.

Closes #212

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test: allocation-free hot-path verification (#217)
…tion

ci: SLSA build-provenance attestation at release (#208, partial)
ci: enforce mutation-score floor as a release gate (#205)
…coyote

test: concurrency / race-condition stress testing with Coyote (#207)
GC.GetAllocatedBytesForCurrentThread was added to .NET Framework in 4.8, so
AllocationFreeTests failed to compile on the net462/net472 targets (CS0117),
breaking the Debug solution build. The original PR only validated net10.0 and
net48 and missed the older framework targets.

Guards the suite with `#if !NET462 && !NET472` — it still runs on net48+ /
netcoreapp3.1+ / net5.0+, which is sufficient since the allocation behaviour under
test is TFM-agnostic. Verified building net462/net472/net48/net10.0.

Refs #217

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…tepinto

test: SourceLink F11 step-into verification (#214)
ci: sustained-load GC / allocation profiling (#212)
fix: guard allocation-free tests for net462/net472 (#217 regression)
…p (SC2010)

actionlint/shellcheck flagged SC2010 (Don't use ls | grep) in the
Generate reproducible-build manifest step added for #225. Select the
first non-symbols .nupkg with a glob loop + case guard instead, and
fail explicitly if none is found.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds behavioural coverage that kills previously-surviving mutants across
the base classes, the pipeline, and Report, then locks in the gain by
raising the Stryker break threshold from 0 to 80.

Base classes (LoaderBase/ExtractorBase/TransformerBase):
- per-run reset of the skipped counter and the run-start timestamp
- StartedAt is a real, recent capture (not default(DateTimeOffset))
- StartedAt is set on the skip-only path
- Dispose()/DisposeAsync() delegate to Dispose(bool)

Pipeline / Report:
- exact exception-message assertions (one-shot run, WithProgress-once,
  aggregate disposal, Report argument guards)
- EstimatedRemaining "remaining <= 0" boundary with a zero rate
- dispose-vs-not branch on the load-progress pipeline path
- per-item progress report in the sink

Result: mutation score 74.40% -> ~86.6%, survivors 86 -> ~43. The
remaining survivors are equivalent mutants (ConfigureAwait(false) with no
SynchronizationContext, GC.SuppressFinalize with no finalizer, the
self-guarding _disposed flag) and timing-only SystemProgressTimer paths
that would require the flaky tests removed in #250.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion-score

# Conflicts:
#	stryker-config.json
…-consumer

docs: consumer-side reproducible-build verification (#225)
…core

test: raise Stryker mutation score to ~87% and ratchet the gate (#205)
Two more real kills plus documentation of the provably-equivalent mutants
that no test can kill.

Real kills (tests):
- StageList aggregates EVERY stage disposal failure (two throwing stages)
  — kills the errors ??= List accumulation mutant.
- Extraction honours a pre-cancelled token even when the source ignores it
  — kills the explicit ThrowIfCancellationRequested in the pipeline head.

Documented equivalent mutants (// Stryker disable once, base classes):
- GC.SuppressFinalize — the types declare no finalizer, so suppressing (or
  not) finalization is unobservable.
- the _disposed guard — the flag has no other reader (nothing throws
  ObjectDisposedException), so it guards an inert base body.
- the EnsureStarted early return — the CompareExchange already guards the
  single assignment, so skipping the fast-path return changes nothing.

Score 86.61% -> 90.12% (over the 90% "high" band). Remaining survivors are
ConfigureAwait(false) flips (differ only under a SynchronizationContext, absent
in the test host), timing-only SystemProgressTimer paths (see #250), and a few
defensive double-guards — left as honest survivors rather than masked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Kills the remaining reachable survivors and documents the rest as
provable equivalents.

Real kills (tests):
- Dispose-stages coverage for the cancellation, progress, and
  progress+cancellation Extract() factory overloads — each has its own
  stages array (kills the three array-initializer mutants that the plain
  IExtractAsync overload's test never reached).

Documented equivalent mutants (// Stryker disable, with inline reasons):
- base-class Dispose(bool)/EnsureStarted blocks — the bodies are inert
  (_disposed has no other reader; CompareExchange already guards the
  single assignment), so removing a block/guard/assignment is unobservable.
- ConfigureAwait(false) flips — no SynchronizationContext in the test host.
- the paired cancellation ThrowIfCancellationRequested checks in the
  pipeline head and sink — mutually redundant defence-in-depth.
- Report.EstimatedRemaining's >= overflow boundary — unreachable exact
  equality.

Score 86.61% -> 96.68%. The 10 remaining survivors are all real,
timing-only behaviour in SystemProgressTimer and the base timer.Start
call — deliberately left as honest survivors rather than masked, since a
deterministic unit assertion would reintroduce the flakiness removed in #250.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…205)

Replaces the ~10 `// Stryker disable` annotations on the ConfigureAwait(false)
awaits with a real test that proves the behaviour they protect: the library
never marshals a continuation back onto the caller's SynchronizationContext
(the property that keeps WPF / WinForms / legacy-ASP.NET consumers safe from
the sync-over-async deadlock).

ConfigureAwait(false)->(true) is invisible to a headless test host (no
context), which is why those mutants survived — but it is NOT equivalent, so
disabling them was the weakest part of the previous pass. New
ConfigureAwaitContextTests installs a counting SynchronizationContext, starts
each operation while it is current, then asserts nothing was posted back.
Six tests cover the streaming and disposal awaits across the EtlPipeline sink,
the DisposingOwned wrapper, the fluent Pipeline/StageList, and LoaderBase's
progress path. A ConfigureAwait(true) mutation posts a continuation to the
context and fails the assertion.

Result: all ConfigureAwait mutants are now Killed by real tests (16 killed,
1 timeout, 0 survived); the 10 annotations are gone. Score 96.68% -> 97.11%.
The remaining 9 survivors are the timing-only SystemProgressTimer / timer.Start
behaviour, still left as honest survivors.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… tests (#205)

Continues turning "disabled" mutants into genuinely tested ones. Removes 7
more // Stryker disable annotations, leaving only mutants that are provably
equivalent in every environment.

- GC.SuppressFinalize (6 mutants, Dispose + DisposeAsync across the 3 base
  classes): FinalizationSuppressionTests derives a finalizer-bearing double,
  disposes it, forces a GC, and asserts the finalizer did NOT run. Dropping
  SuppressFinalize lets it run and fails. (The base types ship no finalizer,
  so the normal cross-TFM run always passes deterministically — finalization
  is suppressed and never runs; the mutant is only exercised under Stryker.)
- Pipeline-head cancellation (EtlPipelineImpl.CountExtracted): AsAsyncEnumerable
  exposes the head with no sink downstream, so its ThrowIfCancellationRequested
  is the sole guard — AsAsyncEnumerable_honours_cancellation_at_the_head kills
  the mutant that the sink's paired guard was masking.

Still documented as equivalent (no test can kill them in any environment):
the base Dispose(bool)/EnsureStarted inert blocks (_disposed has no reader; the
CompareExchange already guards the write), the sink's redundant cancellation
check (the head guard is always upstream), and Report's unreachable >= overflow
boundary.

Score ~97% (96.86%; the small delta vs the prior run is timeout variance from
the GC tests, not a regression — 7 mutants moved from disabled to killed). 362
tests pass. 10 survivors remain, all timing-only SystemProgressTimer/timer.Start.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…core

test: raise mutation score to ~97% — real tests over disables (#205)
Chris-Wolfgang and others added 7 commits July 23, 2026 15:12
…ard (#205)

Adds a real dispose contract to the three base classes: once disposed, every
public entry point (ExtractAsync / LoadAsync / TransformAsync, all overloads)
throws ObjectDisposedException via a new private ThrowIfDisposed().

This gives the Dispose(bool) idempotency flag a real reader, so its guard is no
longer "equivalent because _disposed is never read": DisposedGuardTests now kills
the guard's negation, the _disposed assignment, and the whole-method-body removal
by asserting use-after-dispose throws (every overload, after both Dispose and
DisposeAsync) and that Dispose stays idempotent. The 12 ThrowIfDisposed call sites
are each covered.

Only the guard's `{ return; }` block/return removal stays annotated — dropping it
just re-runs the idempotent `_disposed = true`, which is genuinely unobservable.

Score ~97% (97.06%), 370 tests. Remaining survivors: SystemProgressTimer/timer.Start
timing only. Remaining // Stryker disable: the EnsureStarted early-return, this
guard's redundant block/return, the sink's redundant cancellation check, and
Report's unreachable overflow boundary — all provable equivalents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…core

feat: throw ObjectDisposedException on use-after-dispose; test the guard (#205)
…205)

Audit of the remaining // Stryker disable comments found one that was too
broad: the EnsureStarted annotation wrapped the whole `if`, which also
disabled the condition's Equality mutant — and that one is genuinely
killable. Flipping `Volatile.Read(ref _startTimestamp) != 0` to `== 0` makes
the very first call return early, so the start timestamp is never recorded
and StartedAt stays null; the existing StartedAt/Elapsed tests catch it.

Narrowed the annotation to cover only the fast-path block and its return
(which are equivalent — the CompareExchange below assigns only on the winning
exchange, so a re-entrant caller changes nothing either way). Stryker confirms
the condition mutants are now Killed rather than Ignored.

The other remaining disables were re-audited and are genuinely unkillable:
the Dispose(bool) redundant early-return, the sink's cancellation check (the
CountExtracted head guard is always upstream of CountLoaded via To()), and
Report's overflow boundary (>= vs > differ only at an exact-equality point
that isn't constructible through the division-of-a-division with int-bounded
inputs).

Score 97.08%, 370 tests, 9 survivors — all SystemProgressTimer/timer.Start timing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…core

test: narrow the EnsureStarted disable so its condition stays tested (#205)
Bumps <Version> 0.16.1 -> 0.17.0 (MINOR: new use-after-dispose behavioural
contract) and moves PackageValidationBaselineVersion to 0.16.1, the last
published release.

Highlights since 0.16.1:
- Using a disposed component now throws ObjectDisposedException (the only
  behavioural change; no public signatures added/removed/altered).
- Mutation testing is a release gate at ~97%; the library is verified
  context-agnostic via a counting SynchronizationContext test.
- Concurrency stress (Coyote), allocation-free hot-path verification, GC /
  allocation profiling, SourceLink step-into verification, and consumer-side
  reproducible-build verification.
- SLSA build-provenance attestation (package signing still open — #208).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 00:32

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot 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.

BenchmarkDotNet

Details
Benchmark suite Current: 049e61c Previous: 732efdd Ratio
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_NoProgress(RecordCount: 1000) 34116.18528238932 ns (± 151.90675148325366) 32688.512502034504 ns (± 213.01288290577438) 1.04
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_WithProgress(RecordCount: 1000) 34949.084309895836 ns (± 247.13274941480623) 34913.1728108724 ns (± 92.90601829761779) 1.00
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_NoProgress(RecordCount: 100000) 3357865.2057291665 ns (± 2840.119769792778) 3227835.359375 ns (± 5331.793855491133) 1.04
Wolfgang.Etl.Abstractions.Benchmarks.ExtractorBenchmarks.Extract_WithProgress(RecordCount: 100000) 3437242.5104166665 ns (± 11841.267991002493) 3403880.4401041665 ns (± 29793.796946240178) 1.01
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.FluentPipeline(RecordCount: 1000) 31741.46962483724 ns (± 102.65272885646539) 31655.328531901043 ns (± 117.35331683022335) 1.00
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.ManualComposition(RecordCount: 1000) 32482.84462483724 ns (± 145.51635402879006) 32588.058044433594 ns (± 149.83187729728172) 1.00
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.BaseClassComposition(RecordCount: 1000) 72131.45817057292 ns (± 261.5144950646714) 70625.31201171875 ns (± 1269.4931495812575) 1.02
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.FluentPipeline(RecordCount: 100000) 3945016.59375 ns (± 60435.82406087109) 3210122.2981770835 ns (± 297.86708111041185) 1.23
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.ManualComposition(RecordCount: 100000) 3205289.40625 ns (± 940.718313510232) 3240046.2122395835 ns (± 4670.818271378057) 0.99
Wolfgang.Etl.Abstractions.Benchmarks.PipelineBenchmarks.BaseClassComposition(RecordCount: 100000) 6818775.815104167 ns (± 15473.015289696179) 6896450.057291667 ns (± 10313.052399783468) 0.99

This comment was automatically generated by workflow using github-action-benchmark.

Chris-Wolfgang added a commit that referenced this pull request Jul 24, 2026
Protected-file split for the 0.17.0 release PR (#320), kept to the minimum
that must go in under admin bypass.

Protected (these are what trip `Detect .NET Projects`):
- .github/workflows/{coyote,gc-profile,release,sourcelink-stepinto,stryker}.yaml
- .github/sourcelink/consumer/Directory.Build.{props,targets}

Supporting non-protected files — ONLY for the two workflows that self-trigger
on this PR and would otherwise fail against a main that lacks them:
- stryker-config.json            (stryker.yaml: pull_request on itself + this file)
- tests/...Tests.Concurrency/**  (coyote.yaml: pull_request on itself + this dir)

Deliberately NOT included: tools/GcProfileWorkload/** and the .github/sourcelink
consumer sources. gc-profile.yaml and sourcelink-stepinto.yaml are schedule /
workflow_dispatch only, so they do not self-trigger here and need nothing extra;
those files ride along in #320 under full CI instead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chris-Wolfgang added a commit that referenced this pull request Jul 24, 2026
Protected-file split for the 0.17.0 release PR (#320). Contains ONLY the files
that trip `Detect .NET Projects`, since admin bypass waives every ruleset rule
and anything in here lands without review or CI:

- .github/workflows/{coyote,gc-profile,release,sourcelink-stepinto,stryker}.yaml
- .github/sourcelink/consumer/Directory.Build.{props,targets}

No test code, no configs, no source. In particular:

- tests/...Tests.Concurrency/** stays in #320 — test code must go through review
  and CI, not a bypass.
- stryker-config.json stays in #320 — it carries the break threshold 0 -> 80, and
  merging that ahead of #320's test improvements would point an 80% gate at main's
  ~74% codebase and fail every subsequent PR until the release lands.

Consequence: coyote.yaml self-triggers on this PR and on src-touching PRs to main
until #320 merges, and fails because the Concurrency project isn't there yet.
That check is waived here by the bypass, and #320's own run passes because it
carries the project. Merge #320 promptly to close the window.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Chris-Wolfgang and others added 2 commits July 23, 2026 21:06
…250)

Stage 3 (macOS) failed on the 0.17.0 release PR: countAfterStop != countAfterWait.

StopTimer cannot un-dispatch a callback the thread pool has already queued, and
the test's own comment says so ("at most one" late tick) — but it then asserted
strict equality, relying entirely on the stability window to absorb that tick. On
a starved macOS runner the in-flight callback landed after the 150 ms window
closed, so the equality broke.

- Assert the contract the comment describes: at most one late in-flight callback.
  A timer that never actually stopped fires ~6 more over the 300 ms observation
  window, so a real StopTimer bug still fails loudly.
- Widen the stability window 150 -> 400 ms (maxWait 3000 -> 5000) so the common
  case still absorbs the late tick without needing the grace at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment