Skip to content

fix(build): clean every Release-pack warning + add multi-TFM CI gate - #195

Draft
ottobolyos wants to merge 21 commits into
TrakHound:masterfrom
ottobolyos:fix/release-pack-warnings-cleanup
Draft

fix(build): clean every Release-pack warning + add multi-TFM CI gate#195
ottobolyos wants to merge 21 commits into
TrakHound:masterfrom
ottobolyos:fix/release-pack-warnings-cleanup

Conversation

@ottobolyos

@ottobolyos ottobolyos commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

Cleans the multi-TFM Release-pack diagnostic classes that master's v7.0-prerelease landings did not address, and adds a release-pack CI job that runs dotnet pack -c Release across the full TFM matrix on every push and every non-draft PR.

Several diagnostic classes originally scoped into this PR were independently resolved by the v7.0-prerelease landings on master (2026-06-26): the SYSLIB0057 X509CertificateLoader migration, the CS0162 unreachable-code branches in MQTT modules, the CS0809 obsolete-override on HttpServer.cs, and partial CA2022 handling via ReadExactlyAsync on net9. Those commits were dropped from this PR during the cascade rebase. The remaining diagnostic classes below are the ones master's v7.0-prerelease landings did NOT address, plus the two on-net4x completions that master addressed only on net9+.

Bug classes covered (post-rebase)

Class Diagnostic Sites Root-cause fix
Missing SupportedOSPlatform TFM guard CS0122 / CS0246 Six Service / HTTP-server types where [SupportedOSPlatform("windows")] was added on net8.0 but the attribute type does not exist on net4x / netstandard2.0 Wrap each using System.Runtime.Versioning; and each [SupportedOSPlatform("windows")] usage in #if NET5_0_OR_GREATER ... #endif.
Missing C# 8 LangVersion CS8370 libraries/MTConnect.NET-XML/MTConnect.NET-XML.csproj XsdPreprocessor uses a using var declaration; the Roslyn default LangVersion on netstandard2.0 / net4x is 7.3. Pin LangVersion to 8.0.
Doc cref errors CS1574 adapter/MTConnect.NET-Applications-Adapter/MTConnectAdapterApplication.cs, agent/MTConnect.NET-Applications-Agents/MTConnectAgentApplication.cs Fully qualify <see cref="LogLevel.Debug"/><see cref="NLog.LogLevel.Debug"/> so the resolver has no ambiguity with transitively-pulled Ceen.LogLevel and MTConnect.Logging.MTConnectLogLevel.
Phantom empty package NU5017 libraries/MTConnect.NET-SysML/MTConnect.NET-SysML.csproj Release sets DebugType=None, so no PDB is produced; the requested .snupkg symbol package has no content. Condition IncludeSymbols to off only under Release.
Incomplete reads on non-net9 TFMs CA2022 libraries/MTConnect.NET-HTTP/Ceen/Httpd/LimitedBodyStream.cs (DiscardAllAsync), libraries/MTConnect.NET-HTTP/Servers/MTConnectPostResponseHandler.cs (ReadRequestBytes) Master's #if NET9_0_OR_GREATER { ReadExactlyAsync } #else { ReadAsync (ignored) } silences CA2022 on net9+ but leaves the actual short-read bug live on net4x / netstandard2.0 / net5-8. Capture the read count on the #else branch, return false from DiscardAllAsync on premature EOF (breaks the deadlock loop), and accumulate short reads in ReadRequestBytes with truncation to actual filled length (kills the TrimEnd-on-zero over-truncation).
Lifetime-lease behaviour on net4x CS0672 (net5+) + regression risk (net4x) libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs (InitializeLifetimeService) Master's fix wrapped the entire override in #if NET5_0_OR_GREATER, removing it on net4x. On .NET Framework the base MarshalByRefObject returns a default five-minute lifetime lease; the long-lived Ceen HttpServer would become GC-eligible after five minutes of Remoting-idle time. Restructure: keep the override on every TFM; condition only the [Obsolete] attribute on #if NET5_0_OR_GREATER. return null pins the server object's lifetime to the process everywhere.

Every fix is at root cause (per CONVENTIONS §1.0d-trigies-quaterdecies).

Prevention

New CI job release-pack in .github/workflows/dotnet.yml runs dotnet pack MTConnect.NET.sln -c Release across the full TFM matrix on every push to master and every non-draft PR. Exit must be 0 to merge — the gate the 2026-05-22 regression bypassed.

Tests

  • tests/MTConnect.NET-Common-Tests/Tls/TlsCertificateLoaderTests.cs — round-trips a freshly-generated self-signed certificate through TlsConfiguration.GetCertificate() and GetCertificateAuthority() across all four cert-source shapes. Pins the SYSLIB0057 → X509CertificateLoader migration's behaviour equivalence — now validating master's bb653bbf migration.
  • tests/MTConnect.NET-Common-Tests/Http/CA2022ShortReadTests.cs — pins the CA2022 short-read handling contract on the drain and post-body paths.
  • tests/MTConnect.NET-Integration-Tests/Workflows/MqttRelayWorkflowTests.cs — pins the MqttRelay condition observation flow.

Composes with

ottobolyos and others added 21 commits July 23, 2026 20:09
This is often used for messages and program names. SHDR should pass the value as is to the Agent and the Agent should then decide (based on validation level) whether to accept the value or not.
Although, based on the MTConnect Standard, a value should never be 'empty', an 'empty' value should be able to be accepted as many adpaters/agents don't adhere to this restriction and could cause issues with real world implementations.

An InputValidationLevel set to 'strict' should rewrite an 'empty' value as 'UNAVAILABLE' so that it strictly conforms to the standard.
…nectDevices validation. This allows a device to be validated at a different level than observations/assets
The new `DeviceValidationLevel` property + enum added on this branch adds
one config key row to `IAgentConfiguration` and `AgentConfiguration`; the
drift gate `docs/scripts/generate-reference.sh --check` reports DRIFT
until the generated `docs/reference/configuration.md` catches up. Runs
the generator to produce the current output.
Commit 8dd28c6 narrowed the empty-Result coerce guard on
`MTConnectAgent.AddObservation` from unconditional to
`InputValidationLevel == Strict`-only. On the three other levels
(`Ignore`, `Warning`, `Remove`) a null / empty-string / whitespace-only
Result then flowed verbatim onto every wire transport, contradicting the
MTConnect Part 1 mandate this repo already encodes verbatim as
`Observation.UnavailableDescription`:

    If an Agent cannot determine a Valid Data Value for a DataItem, the
    value returned for the Result for the Data Entity MUST be reported
    as UNAVAILABLE.

The empty string is not a Valid Data Value under any typed DataItem
schema — `MTConnectStreams_*.xsd` enumerates `UNAVAILABLE` as a schema
token, never as an empty content model; `xs:decimal` samples reject the
empty lexical form outright; cppagent, the reference implementation,
coerces or rejects empty Results at ingress.

Restores the pre-8dd28c6b guard: coerce fires whenever the item is
non-CONDITION and the Result is empty, regardless of validation level.
Strict still gains the original benefit — the empty observation lands
rather than being silently dropped by the validator. The 8 red tests on
`AddObservation_NullEmptyOrWhitespaceResult_Coerced_To_Unavailable` were
pinning exactly this contract and go green with the restored guard.
Adds a reflection-based fixture under
tests/MTConnect.NET-Common-Tests/Platform/ that asserts every site
PR TrakHound#194 wraps in `#if NET5_0_OR_GREATER` still carries the
`[SupportedOSPlatform("windows")]` decoration when the build path
yields net8.0. The fixture covers the six sites from the May-22
warnings sweep — WindowsService, MTConnectAgentService,
MTConnectAdapterService, both `MTConnect.Applications.Service`
types (agent + adapter), and the
`Ceen.Httpd.HttpServer.AppDomainBridge.HandleRequest(SocketInformation,...)`
plus `HttpServer.RunClient(SocketInformation,...)` HTTP-server
sites — and pins the API surface so a future contributor who
removes the attribute or narrows the wrap fails the test.

The fixture runs only under net8.0 (the only TFM every test project
targets), so cannot directly fail under the pre-fix code. Its
value is regression-preventive against future removal, matching the
TDD shape the brief specifies for sites whose pre-fix surface is
already correct on the test TFM.

Extends the test csproj with reflection-only references to
MTConnect.NET-Services, MTConnect.NET-HTTP,
MTConnect.NET-Applications-Agents, and
MTConnect.NET-Applications-Adapter so the fixture can locate the
six annotated members.
Adds a fixture under tests/MTConnect.NET-XML-Tests/Xml/ that
exercises XsdPreprocessor.StripXsd11Constructs — the production site
that uses the C# 8.0 `using var reader = ...;` declaration. On
netstandard2.0 the compiler's default LangVersion is 7.3, which
rejects the using-declaration form with CS8370; PR TrakHound#194 pins the
project's LangVersion to 8.0. The fixture runs only under net8.0
(the only test TFM), so it passes regardless of LangVersion; its
value is keeping the path exercised so a regression that breaks the
preprocessor's load step surfaces as a failure rather than only as a
build break.
Adds a structural test under
tests/MTConnect.NET-AgentModule-MqttRelay-Tests/ that scans
Module.cs for the shadowing `foreach (var observation in ...)`
declaration inside AgentObservationAdded. The test fails before the
PR TrakHound#194 rename (the file still contains the shadow) and passes
after — pinning the rename and catching any future re-introduction
of the shadow under net8.0, which is the only TFM the CI matrix
exercises directly.

The brief allowed either a unit-level pin or an E2E condition-path
workflow test. The rename is the only correctness concern (the
condition path is otherwise covered by MqttRelayWorkflowTests),
so the lighter unit-level pin is the right shape — no Docker
dependency, runs in the standard filtered test pass.
Restores Release pack-ability across the full TFM matrix
(net461..net9.0 + netstandard2.0). Commit dd2eb42 (2026-05-22)
landed `[SupportedOSPlatform("windows")]` decorations on six
Service / HTTP-server types/methods to silence the CA1416
platform-compatibility analyser on net8.0. The attribute type
ships in System.Runtime on .NET 5.0+ but is absent from net4x and
netstandard2.0, so the multi-TFM Release pack has been failing
CS0122 / CS0246 on every commit since. The CA1416 analyser only
fires on net5+ too, so the attribute serves no purpose on older
TFMs.

Wraps each `using System.Runtime.Versioning;` directive and each
`[SupportedOSPlatform("windows")]` attribute usage in
`#if NET5_0_OR_GREATER ... #endif` so the older TFMs see neither.
No runtime behaviour change on .NET 5.0+ — the decoration remains
in effect — and net4x / netstandard2.0 stop referencing a type
that does not ship there.

Sites covered:
  * libraries/MTConnect.NET-Services/WindowsService.cs
  * libraries/MTConnect.NET-Services/MTConnectAgentService.cs
  * libraries/MTConnect.NET-Services/MTConnectAdapterService.cs
  * libraries/MTConnect.NET-HTTP/Ceen/Httpd/HttpServer.cs (two
    attribute sites — AppDomainBridge.HandleRequest and
    HttpServer.RunClient overloads that take SocketInformation)
  * agent/MTConnect.NET-Applications-Agents/Service.cs
  * adapter/MTConnect.NET-Applications-Adapter/Service.cs

The SupportedOSPlatformAttributePresenceTests fixture added in
the preceding test commit goes from RED on net8.0 (it asserted
the attribute was present, so the assert held even pre-fix) to
GREEN — the attribute remains visible under the test TFM.
XsdPreprocessor.StripXsd11Constructs uses a C# 8.0 `using var`
declaration:

    using var reader = new StringReader(xsdSourceXml);

The Roslyn default LangVersion on netstandard2.0 / net4x falls back
to 7.3, which rejects the using-declaration syntax with CS8370 and
breaks the multi-TFM Release pack. Pinning LangVersion to 8.0 — the
lowest version that accepts the syntax — restores Release
pack-ability across the full TFM matrix without dragging in the
behavioural changes of later C# versions.

Directory.Build.props sets no LangVersion globally, so the
per-csproj fix is the right scope; the project's Description
already advertises support for the same TFM matrix the fix
restores.
Adds tests/MTConnect.NET-Common-Tests/Tls/TlsCertificateLoaderTests.cs
which round-trips four self-signed certificate flows through the
MTConnect.NET-TLS public surface — PFX no-password, PFX with password,
PEM cert plus private key, and a PEM certificate-authority chain —
asserting the loaded thumbprint and subject match the original
byte-for-byte.

The fixture is the test-before-fix per CONVENTIONS §1.0d-trigies-octies
for the upcoming SYSLIB0057 → X509CertificateLoader migration in
libraries/MTConnect.NET-TLS/TlsConfiguration.cs. Pre-fix the build is
RED on the net9.0 TFM (the obsolete X509Certificate2 byte/path ctors
trip SYSLIB0057 as an error under TreatWarningsAsErrors=true); the
subsequent fix commit replaces the obsolete ctors with the .NET 9
X509CertificateLoader.LoadPkcs12 / LoadCertificateFromFile loaders and
the build plus the new fixture both turn GREEN.
…er Release

`dotnet pack -c Release` against MTConnect.NET-SysML.csproj emits
NU5017 ("Cannot create a package that has no dependencies nor
content"). Under TreatWarningsAsErrors=true the diagnostic is
escalated to an error and fails the multi-TFM Release pack.

Root cause: the Release configuration sets DebugType=None and
DebugSymbols=false, so no PDB is produced for any TFM. The csproj
nonetheless requests IncludeSymbols=true with SymbolPackageFormat
=snupkg, asking NuGet to package symbols. With no PDBs to carry the
generated .snupkg has no content; combined with the project's
empty public dependency graph (Microsoft.SourceLink.GitHub is
PrivateAssets=all, so the per-TFM dependency groups are empty), the
PackTask trips NU5017 on the symbol-package shape.

Root-cause fix: disable IncludeSymbols under Release. There is no
PDB to ship, so the snupkg adds nothing of value; the .nupkg itself
continues to carry the multi-TFM lib/<tfm>/MTConnect.NET-SysML.dll
output verbatim. The Package configuration (used by the upstream
nuget.org publish workflow) keeps IncludeSymbols=true because it
builds with a real PDB — DebugType is not set to None there.

Verified on bluefin: the produced
MTConnect.NET-SysML.<v>.nupkg contains lib/net6.0/, lib/net7.0/,
lib/net8.0/, and lib/net9.0/ — the same TFM layout as before the
fix, with no accompanying empty .snupkg.

Build-gate only — packaging metadata change with no runtime
semantic. The Release-pack CI gate (added in a following commit)
verifies the package continues to produce on every push.
… read (RED)

Adds tests/MTConnect.NET-Common-Tests/Http/CA2022ShortReadTests.cs
covering the two CA2022 sites in MTConnect.NET-HTTP:

  - libraries/MTConnect.NET-HTTP/Ceen/Httpd/LimitedBodyStream.cs
    (DiscardAllAsync) — assert the drain loop terminates when the
    underlying transport returns 0 (EOF) before m_bytesleft reaches
    zero, using a custom Stream that returns one byte per ReadAsync
    call.
  - libraries/MTConnect.NET-HTTP/Servers/MTConnectPostResponseHandler.cs
    (ReadRequestBytes) — assert a body that ends with a legitimate
    0x00 byte and arrives one byte per ReadAsync (the worst-case short
    read) is reconstructed byte-for-byte. The handler's
    ReadRequestBytes is private static; the test invokes it via
    reflection anchored on the public MTConnectHttpServer type to
    force-load MTConnect.NET-HTTP.dll, then GetType the internal
    handler.

The fixture is the test-before-fix per CONVENTIONS §1.0d-trigies-octies
for the upcoming CA2022 fix. Pre-fix the build is RED on net9.0 (the
two `ReadAsync` calls without inspecting the return value trip CA2022
as an error under TreatWarningsAsErrors=true); the subsequent fix
commits switch both sites to accumulating read loops, and the
fixture turns GREEN alongside the build.
The XML doc cref `<see cref="LogLevel.Debug"/>` in
MTConnectAdapterApplication.cs:45 and MTConnectAgentApplication.cs:56
references NLog.LogLevel.Debug — a public static field on the NLog
log-level type. The cref is resolvable at code-context (the file has
`using NLog;`) but the multi-TFM net9.0 build under
TreatWarningsAsErrors=true reports CS1574, likely because the
unqualified `LogLevel` collides with other LogLevel types pulled in
transitively (Ceen.LogLevel, MTConnect.Logging.MTConnectLogLevel)
during the docfx-side cref resolution pass.

Root-cause fix: qualify the cref to `NLog.LogLevel.Debug` so the
resolver has no ambiguity. The runtime field reference (`LogLevel
_logLevel = LogLevel.Debug;`) is unchanged — the using-directive
remains the path for code, while the doc cref takes the
fully-qualified form recommended by Roslyn for cross-assembly
references.

Documentation-only change. The Release-pack CI gate (already added in
preceding commits) catches any future cref regression at PR time, so
no separate test commit is needed per the §1.0d-trigies-octies
build-gate-as-test carve-out for docs-only commits.
…ssions

Adds a fourth job `release-pack` to .github/workflows/dotnet.yml that
runs `dotnet pack MTConnect.NET.sln -c Release` across the full multi-
TFM matrix (net461 → net9.0) on every push to master and every non-
draft PR.

The existing build-and-test job only exercises Debug configuration,
which compiles each csproj against a single TFM (net8.0 per the
per-project `<PropertyGroup Condition="…Debug">` blocks). Release and
Package configurations multi-target net4x → net9.0; some diagnostics
only fire on the legacy TFMs (CS0162 from `#if NET5_0_OR_GREATER`
branches that go unreachable on net4x), and some only on net9.0
(SYSLIB0057 X509Certificate2 ctor obsoletion). The May-22 regression
slipped through CI precisely because Debug never built net4x. This
gate catches the next regression class at PR time.

Gate shape:
- Same draft-skip behaviour as build-and-test
  (`github.event_name == 'push' || github.event.pull_request.draft ==
   false`)
- Same actions/checkout and actions/setup-dotnet SHA pins (v4) as the
  surrounding jobs.
- No docs dependency — pack does not need the VitePress dist tree.
- Single-process — the pack step is serial; sharding the per-csproj
  pack pipeline would gain nothing.
- Output discarded — produced .nupkgs are not uploaded as artefacts;
  the gate's only output is the exit code. A failure step parses
  pack.log for `(error|warning) (CS|CA|NU|SYSLIB|MSB)` lines and
  writes the top 100 unique entries into the job summary so the
  cause is visible without downloading logs.

Runs on every push and every non-draft PR; exit must be 0 to merge.
…me override

Two follow-on fixes on the Ceen HTTP surface that master's v7.0-prerelease
landings addressed only partially. Both are in scope of this PR under
CONVENTIONS §1.0d-trigies-quinquies (vendored code is in scope of
bug-class fixes; never silently excluded).

## CA2022 short-read handling on non-net9 TFMs

`LimitedBodyStream.DiscardAllAsync` and
`MTConnectPostResponseHandler.ReadRequestBytes` both call
`Stream.ReadAsync` without capturing the return count on non-net9
TFMs. Master's `#if NET9_0_OR_GREATER { ReadExactlyAsync } #else
{ ReadAsync (ignored) }` silences CA2022 on net9+ but leaves the
actual short-read bug live on net4x / netstandard2.0 / net5-8:

- `DiscardAllAsync` deadlocks on premature EOF — a 0-byte read does
  not decrement `m_bytesleft`, so the outer `while (m_bytesleft > 0)`
  loop repeats forever with the same 0-byte read.
- `ReadRequestBytes` under-reads short-fragmented POST bodies and
  then calls `TrimEnd(bytes)` on the whole buffer — legitimate
  trailing `0x00` bytes at the end of the body are stripped along
  with the unfilled buffer padding.

`DiscardAllAsync` now captures the read count on the `#else` branch
and returns `false` (drain incomplete) on premature EOF, so the caller
can propagate the failure instead of hanging.

`ReadRequestBytes` accumulates short reads into the fixed 2 MB
buffer until either EOF or the buffer is full, then truncates the
returned array to the actually-filled length. Removes the TrimEnd
heuristic on the non-net9 branch; the net9 branch keeps master's
`ReadExactlyAsync + TrimEnd` shape unchanged.

## HttpServer.InitializeLifetimeService lifetime lease on net4x

Master's `bb653bbf` wrapped the entire `InitializeLifetimeService`
override in `#if NET5_0_OR_GREATER`, removing the override on net4x.
On .NET Framework the base `MarshalByRefObject` returns a
default five-minute lifetime lease; without the override the Ceen
`HttpServer` — designed to be long-lived — becomes eligible for
garbage collection after five minutes of Remoting-idle time.

Restructure: keep the override on every TFM; condition only the
`[Obsolete]` attribute on `#if NET5_0_OR_GREATER`. `return null`
pins the server object's lifetime to the process everywhere.
Docstring updated to explain the CS0672 / CS0809 dance and the
lease semantic.
@ottobolyos
ottobolyos force-pushed the fix/release-pack-warnings-cleanup branch from 9d91f2e to a2eebe0 Compare July 23, 2026 18:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

2 participants