Commit Graph

10 Commits

Author SHA1 Message Date
romanett 61edaa1ed0 Remove netstandard2.1 and net472 target frameworks (#4624)
# Description

Drops support for **netstandard2.1** and **.NET Framework 4.7.2**.
Libraries now target `net48`, `net8.0`, `net9.0` and `net10.0`.
`Opc.Ua.Types` and `Opc.Ua.SourceGeneration.Core` still also target
`netstandard2.0`, but only because the Roslyn source generators load
them. There is no general netstandard2.0 support.

**Preprocessor directives** (112 files)
- Every `#if`/`#elif` mentioning `NETSTANDARD2_1*`, `NET472*`,
`NET_STANDARD_TESTS`, `SKIP_ECC_CERTIFICATE_REQUEST_SIGNING` or
`NETSTANDARD*` was evaluated against the remaining TFM set (net48,
net8/9/10, plus netstandard2.0 inside `Opc.Ua.Types`/`tools`) and
simplified. Dead branches were removed.
- Example: `NETSTANDARD2_1 || NET8_0_OR_GREATER` → `NET8_0_OR_GREATER`.
- Example: `NET8_0_OR_GREATER || NET472_OR_GREATER` → removed (always
true).
- Netstandard-only code was removed: `UnixFileModeShim`, the CRDT
reflection path in `ReplicatedGossipOptions.CreateReaderOptions`, and
the netstandard2.1-only test variants.

**Custom test logic**
- `targets.props` drops the `net472`, `netstandard2.0` and
`netstandard2.1` `CustomTestTarget` profiles, `NetStandardTests` and
`SKIP_ECC_CERTIFICATE_REQUEST_SIGNING`. Supported values are now
`net48`, `net8.0`, `net9.0`, `net10.0`.
- The `NET_STANDARD_TESTS` define is removed from 29 test projects.
- The "build netstandard shells as net10.0" overrides are removed from
18 projects.
- `RestrictForLegacyTfm` now only applies to `net48`.
- `tests/customtest.bat` is updated to the new values.

**Build / packaging / CI**
- MSBuild TFM conditions are simplified: Kestrel package choice, MQTTnet
v4 (net48 only), BouncyCastle, Di/OpenUsd/Vision/Stress test excludes,
and `System.IO.FileSystem.AccessControl` (dropped from Core).
- The MigrationAnalyzer nuspec drops its net472/netstandard2.1 lib
groups. The Azure signing lists are updated.
- `get-ci-matrix.ps1` removes the net472/netstandard test profiles
(including the PR-gating `linux-netstandard2.1`) and the matching build
legs.

**Breaking API change (2.0 preview):** removed
`HttpsTransportListener.IsWssTransportSupported` and
`ReplicatedGossipOptions.IsGossipStateDecodingSupported`. They only
returned `false` on netstandard2.1 and are now always `true`. The test
guards that used them were removed as well.

**Docs**
- NuGet READMEs, `README.md`, `DeveloperGuide.md`, `PubSub.md`, the
certificate/ECC/identity docs and the 2.0 migration guide (plus its
`.agents` skill mirror) are updated.
- The migration guide notes that 4.7.2 consumers must move to 4.8 and
netstandard2.1 consumers to net8.0+.

### Reviewer notes
- The `System.IO.FileSystem.AccessControl` `PackageVersion` pin is left
in `Directory.Packages.props`. It has no direct reference any more, but
transitive pinning is on, so it was kept to avoid shifting resolved
versions.
- The `#if` rewrite was done mechanically by a truth-table evaluator.
The diff in the `.cs` files is limited to directive lines, removed dead
branches, and stale-comment updates.

### Verified locally
- `dotnet build UA.slnx -p:CustomTestTarget=net48`: 0 errors.
- `dotnet build UA.slnx -p:CustomTestTarget=net10.0`: 0 errors.
- Default multi-target builds (no `CustomTestTarget`) of Opc.Ua.Types,
Bindings.Https, PubSub.Mqtt and Redundancy.Server succeeded.
- net10.0 tests:
  - Opc.Ua.Redundancy.Server.Tests: 840/840 passed.
  - Opc.Ua.Types.Tests: 11363 passed, 1 skipped.
  - ModelDependencyScanner tests: 20/20 passed.
- The full test suite was **not** run locally.

## Related Issues

- None

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [ ] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 17:09:49 +02:00
Marc Schier e9313ef957 Only use GitHub action for CI and nightly, establish release-branch-only package publication (#4513)
## Description

Two related changes to how the 2.0 line is built and shipped.

### 1. Release-branch-only package publication

- `master` produces development previews only.
- Stable packages can only be built from a canonical
`release/<major>.<minor>` branch and are promoted manually from the
exact signed candidate artifact.
- Core packages use explicit `major.minor.patch` versions (`2.0.0`,
`2.0.1`, then `2.1.0`); git height never changes a stable package patch.
- XRegistry, WoT Connectivity, Vision, Robotics, Redundancy,
Positioning, OpenUSD, ISA95, AI, DI, the relevant MCP extensions, and
OpenUSD connector tools remain `-preview.N` while following the core
numeric version.
- nuget.org receives only non-`.Debug` package IDs; GitHub Packages
retains the complete signed Release/Debug candidate.
- `docs/ReleaseProcess.md` is a reproducible runbook for developers and
agents, with each step stating its completion evidence.

Promotion is source-bound and byte-checked: `release.yml` checks out
nothing until the candidate SHA is known, and
`assert-published-packages-match.ps1` proves that any version already on
a feed holds *this* candidate's content before `--skip-duplicate` is
allowed to skip it. Canonical release-branch naming is matched
case-sensitively with no leading zeroes, and `version.json`'s
`publicReleaseRefSpec`, both policy helpers and the copy `release.yml`
inlines are pinned to one expression by tests.

### 2. PR CI moved to GitHub-hosted runners

- `.github/workflows/buildandtest.yml` (**CI**) now owns pull-request
validation outright, covering the Windows Debug/Release net48 + net10.0
solution builds and fast tests that only Azure Pipelines ran, alongside
the existing Linux/macOS, all-target and NativeAOT legs. `build-and-test
summary` remains the stable required check name.
- `.github/workflows/nightly.yml` is a new **manual-only**
(`workflow_dispatch`) full-scope workflow covering the Azure nightly
workload: all solutions x seven target profiles x Debug/Release, the
net472/netstandard profiles, Windows + Linux NativeAOT, and the
long-running and durable tiers.
- Shared deterministic discovery/batching lives in
`.github/scripts/get-ci-matrix.ps1` with a local composite action, so
both entry points expand the same workload manifest instead of
duplicating a large embedded script.
- `azure-pipelines.yml` is set to `pr: none`.
- The standalone pump sample image workflow is folded into the
consolidated **Images CI** workflow, which now builds all ten images and
publishes an `images summary` rollup suitable as a required check.

## Related Issues

No related issue provided.

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.

## Validation

- `Opc.Ua.Tools.Tests` full `net10.0` suite: **959 passed, 1 skipped, 0
failed**.
- Version/package-policy regression subset
(`PackageVersionPolicyTests`): **60 passed, 0 failed**.
- GitHub Actions **CI** run 35719807620: **149/149 jobs green** on
GitHub-hosted runners.
- Canonical release-branch rule verified against the pinned nbgv 3.7.115
by building throwaway branches: `release/2.0`, `release/0.9`,
`release/10.42` -> `PublicRelease=True`; `release/02.0`, `release/2.00`,
`Release/2.0` -> `False`.
- Package identity gate verified against a live nuget.org package:
served packages carry a repository `.signature.p7s`, so equality is
decided on content excluding that part. Re-signing and zip reordering
produce an identical digest; a one-byte content change does not.
- Synthetic Release-only, Debug-only, combined, preview, duplicate,
orphaned-symbol, mismatched-root and accidental-stable-family package
sets exercise the shared validator.
- Stable-release rehearsal in an isolated `release/2.0` clone:
`Opc.Ua.Di` packed as `2.0.0-preview.6` depending on stable Core
`2.0.0`; `Opc.Ua.Di.Client` likewise against stable Client `2.0.0`.
- All workflow YAML, `version.json`, the package-policy PowerShell
scripts and the internal release-runbook anchors parse.

## Known validation limits

- Full `UA.slnx` `net48` and `net10.0` local validation remains blocked
by pre-existing `NETSDK1005` and dependency-version failures in
unmodified fuzz/sample projects; the corresponding checklist items
intentionally stay open.
- The TRX-driven test verdict added for the intermittent at-exit host
stall is proven by `CiTestVerdictTests`; the stall itself has not
recurred since.
- `nightly.yml` has not yet been run against a full-scope Azure run for
tuple-level parity — see below.

## Operational follow-up

This remains a preparation PR targeting `master`. It does **not**
authorize a release.

Release sequence:

1. Merge this PR.
2. Create canonical `release/2.0`, advance `master` to
`2.1.0-preview.{height}`, and open a separate `2.0.0` version PR against
`release/2.0`.
3. Follow
[`docs/ReleaseProcess.md`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/ReleaseProcess.md)
for candidate dry-run, environment approval, promotion, tag/release
finalization, and next-patch preview setup.

CI migration steps still needing administrator access are tracked in
[`docs/DeveloperGuide.md#migration-status`](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/DeveloperGuide.md#migration-status):
proving `nightly.yml` parity on a trusted SHA with the private fuzz
corpus provisioned, turning off definition 14's service-side PR
override, retiring the Azure full-scope schedules and push trigger, and
retiring the duplicate `azure-pipelines-preview.yml` publisher.

> **Before cutting `release/2.0`:** the `Release` ruleset requires the
Azure context for *every* `refs/heads/release/*`, but a new 2.x branch
inherits `pr: none`, so that context would never report and would block
every PR into the new line. Exclude the new ref from that ruleset, or
add a 2.x ruleset requiring `build-and-test summary`, as part of
creating the branch.

---------

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-23 15:47:43 +00:00
Marc Schier f5ad124891 Additional fuzzing plus fixes for every defect the campaigns found (#4459)
# Description

Brings the stack's fuzz testing to parity with the fork
(`aio-opcua-sdk`), then fixes every defect the resulting campaigns
exposed.

The fork's fuzzing was used as the behavioural baseline: all **28
fork-local callbacks** and **13 OneFuzz registrations** are now
accounted for by an explicit mapping, not just by a larger target count.
The work extends the existing SharpFuzz/NUnit infrastructure rather than
introducing another framework.

### Coverage

- **84 continuous libFuzzer targets** and **81 AFL adapters** across
Encoders, Certificates, Network and PubSub.
- **PubSub area integrated** (Corpus/Tools/Tests siblings, solution
entries) — it previously had host source only and was absent from the
solution and replay infrastructure.
- Segmented binary adapters, binary-to-JSON and XML-to-JSON conversions,
standalone certificate loading, both chain-parser variants, CRL encoding
and canonical TBS round-trips.
- The 12 original fork message inputs are retained byte-identically
alongside richer deterministic generators; the three historical
crash/slow/timeout reproducers are unchanged (verified identical by
SHA-256).

### Production fixes found by fuzzing

These are real robustness and data-integrity bugs, each with a retained
regression input:

- `UadpBinaryReader.Advance` accepted attacker-controlled lengths on the
receive path. Declared payload sizes are now authoritative bounded
sections, with negative-count rejection and overflow-safe reassembly
arithmetic.
- PubSub JSON decoding leaked `InvalidOperationException` for invalid
UTF-8.
- `JsonEncoder.WriteExtensionObject` leaked
`System.Text.Json.JsonReaderException`.
- `JsonEncoder` silently emitted a lossy `{}` for an `ExtensionObject`
whose TypeId could not be represented in JSON.
- `JsonDecoder` wrapped a bodyless `ExtensionObject` envelope as a raw
JSON body instead of preserving `ExtensionObjectEncoding.None`.
- `Node.Encode` lost the null/empty collection distinction.
- Binary/XML Variant matrix dimensions are prevalidated rather than
relying on a caught `ArgumentException`.
- `X509CRL.ReadTime` pre-validates strict RFC 5280 whole-second Zulu
time forms.
- `Utils.ParseCertificateChainBlob` disposes correctly on partial-chain
failure.
- **`nodeid.dict` was unloadable by libFuzzer** (empty token, unescaped
quote). libFuzzer rejects the whole file on one bad line, so four parser
targets silently executed **zero** inputs. Fixed and now guarded by
dictionary validation.

### Infrastructure

- `fuzzing/fuzz-targets.json` manifest recording adapters, corpora,
dictionaries, oracles and fork-behaviour mappings, with
`validate-fuzz-manifest.ps1` and **27 negative controls**.
- Strict host/tools discovery and replay: `--list`, `--replay`, non-zero
exits on unknown targets, missing or empty corpora, and failed callbacks
(playback previously logged failures and returned success).
- A real process watchdog for the timeout/slow regressions — NUnit
`CancelAfter` cannot interrupt a synchronous fuzz callback.
- Runner scripts made non-destructive (unique work directories, no
generic `bin`/findings deletion) and corrected to the native driver's
single-`--target_arg` apphost contract.
- Opt-in `OneFuzz=true` class-library publication producing a
relocatable, **uninstrumented** drop, validated by exact callback
discovery, dependency closure, corpora and dictionaries.
- CI: fuzz replay matrix plus a public drop gate, triggered by corpus-,
dictionary- and script-only changes.

### Verification

| Gate | Result |
|---|---|
| Bounded campaigns, all 84 targets (1,000 inputs, seed 1) | 84/84,
84,000 executions, zero findings |
| OneFuzz drop | 84/84 published callbacks replayed, zero skips |
| Fuzz replay net10.0 / net8.0 / net9.0 | 9,470 passed each, 0 failed |
| Fuzz replay net48 | 8,996 passed, 0 failed (Network/PubSub are
modern-only) |
| `Opc.Ua.Types.Tests` / `Opc.Ua.Core.Tests` | 9,800 and 4,442 passed, 0
failed |
| Retained regression assets | 132 `crash-*` inputs replayed by every
target each run |

### Known gaps (deliberately not closed)

- **Fidelity is not enforced against the pipeline crash corpus.** The
differential binary-to-JSON targets assert both robustness (no unhandled
exception, expected of arbitrary bytes) and fidelity (a decoded value
re-encodes equivalently, a property of well formed values). The pipeline
supplies 22,293 blobs collected for the targets that existed before this
PR, none of which asserted fidelity; 522 of them violate it, and that
count did not move across two rounds of genuine fixes because each fix
only lets the mutator reach the next representability gap. Rather than
weaken the assertion, the two contracts are separated: fidelity failures
raise `EncodingFidelityException`; curated assets under `Assets/Repo`,
the curated `Testcases` corpus and every continuous libFuzzer campaign
stay strictly enforced, while externally supplied assets are gated on
robustness only and their fidelity findings are reported with counts and
bounded base64 reproducers.
- **Internal MSFT OneFuzz canary has not been run.** It needs an
owner-supplied ownership profile, pipeline authorisation and corpus
snapshot; none were available. The drop builds and validates locally and
records `serviceCanaryPerformed: false`. A build or upload is not a
successful worker run.
- **Legacy JSON wire forms are a partial mapping.** The fork accepts its
four original `TypeId`/`Body` messages; this stack rejects that retired
envelope (measured JSON-only control: fork `JsonEncoder` 630/1356 lines
vs 0/1007 here). Recorded as an explicit unresolved mapping rather than
an exemption. `LegacyNonReversible` is a RawData-derived profile and is
not counted as independent coverage.

## Related Issues

_No tracking issue was supplied for this work; happy to open one and
link it if maintainers prefer._

- Fixes #github-issue-number, ...

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

> Note on the checklist: the fuzz replay matrix, `Opc.Ua.Types.Tests`
and `Opc.Ua.Core.Tests` were run locally on **net48** and **net10.0**
(plus net8.0/net9.0) and all passed. CI and CodeQL results are not yet
available for this branch, so those boxes are left unchecked. The CLA
box is for the author to confirm.
2026-09-10 05:56:31 +02:00
Marc Schier e73e71184d [WotCon] Add OPC UA WoT Connectivity 1.1 with the Types, source generation, Server and xRegistry work it builds on (#4128)
## Summary

Adds OPC UA WoT Connectivity 1.1 to the stack, together with the Types,
source generation, Server and xRegistry work it is built on. This branch
is the merge point for the fourteen-PR stack that replaced integration
PR #4093; every constituent PR was reviewed and approved separately and
is listed below.

414 files changed, +108,936 / -3,199. Roughly 46k added lines of product
code, 57k of tests, plus samples, tools and documentation.

## Types

- Complete `ExtensionObject` raw-body decoding for binary, XML and JSON
bodies by resolving the concrete type through the message context's
encodeable factory, and simplify the `Variant` structure helpers by
delegating to it (#4128).
- Add `IAtomicFileReplace` as an optional `IFileSystem` capability so
existing external implementations keep working, with atomic publish for
`LocalFileSystem` via `File.Replace`/`File.Move` and for
`VirtualFileSystem` by re-keying the in-memory entry (#4128).
- Add lossless conversion between WoT documents and NodeSet2
(`Opc.Ua.Wot.WotNodeSetConverter`). A byte-exact `uav:nodeSet` envelope
is preserved when requested, the structured `uav:nodes` projection is
used when the readable vocabulary is incomplete, and NodeSet2 is
otherwise synthesized from readable WoT terms. Unmapped JSON members
survive a round trip as pointer-addressed residue (#4131).

## Source generation

- Improve the NodeSet to ModelDesign conversion and the generated node
state (#4132).
- Generate OPC UA models directly from WoT Thing Description files
(#4134).

## Server

- Add live NodeManager shadow and immediate reload. A reload
materializes a new generation beside the active one and switches
atomically; the superseded generation is retired gracefully so existing
MonitoredItems keep being served until they drain, or immediately when
the caller asks for it. Includes the request admission and drain
machinery that lets an orderly shutdown wait for admitted requests
instead of tearing down underneath them (#4147).
- Make subscription transfer between sessions transactional, so a failed
transfer leaves neither session holding a partially moved subscription
(#4150).

## xRegistry

- Allow clients to use explicit registry roots rather than assuming a
well-known location (#4130).

## WoT Connectivity

- Add the WoT Connectivity 1.1 information model: a registry-first
revision layered on the abstract xRegistry base model, incorporating the
complete published OPC 10100-1 v1.02 surface as deprecated nodes in the
same namespace so existing 1.02 clients keep working (#4139).
- Add the protocol binding abstractions and planners that compile WoT
forms into executable plans (#4140).
- Add the HTTP, Modbus, OPC UA and MQTT binding executors (#4144).
- Add the registry and its stores, including document versioning,
validation, dependency resolution and refresh (#4146).
- Add the registry client (#4142).
- Materialize WoT Thing Descriptions into the server address space,
deriving types from Thing Models and instances from Thing Descriptions
(#4154).
- Restore the WoT asset registry bridge so the deprecated 1.02 asset
surface is backed by the registry (#4171).
- Add the WoT Connectivity samples and complete the documentation
(#4156), and restore the remaining coverage and docs (#4172).

## New projects

`src/Opc.Ua.WotCon.Bindings`, `src/Opc.Ua.WotCon.Bindings.Mqtt`,
`tests/Opc.Ua.WotCon.Bindings.Tests`,
`tests/Opc.Ua.WotCon.Samples.Tests`, and the `samples/WotCon`
AggregationServer, AggregationClient and FlatTagServer samples.

## Constituent pull requests

Merged into this branch top-down, each independently approved:

| PR | Title |
| --- | --- |
| #4128 | [Types] Complete ExtensionObject decoding and add an atomic
file replace capability |
| #4131 | [Types] Add lossless conversion between WoT documents and
NodeSet2 |
| #4132 | [SourceGeneration] Improve NodeSet to ModelDesign conversion
and generated node state |
| #4134 | [SourceGeneration] Generate OPC UA models from WoT Thing
Description files |
| #4147 | [Server] Add live NodeManager shadow and immediate reload |
| #4150 | [Server] Make subscription transfer between sessions
transactional |
| #4130 | [XRegistry] Allow clients to use explicit registry roots |
| #4139 | [WotCon] Add the WoT Connectivity 1.1 information model |
| #4140 | [WotCon] Add the WoT protocol binding abstractions and
planners |
| #4144 | [WotCon] Add the HTTP, Modbus, OPC UA and MQTT binding
executors |
| #4146 | [WotCon] Add the WoT Connectivity registry and its stores |
| #4142 | [WotCon] Add the WoT Connectivity registry client |
| #4154 | [WotCon] Materialize WoT Thing Descriptions into the server
address space |
| #4156 | [WotCon] Add the WoT Connectivity samples and complete the
documentation |
| #4171 | Restore WoT asset registry bridge |
| #4172 | Restore WoT coverage and docs |

Supersedes #4093.

## Follow-up

The WoT Connectivity and WoT Binding drafts have moved to 1.1-draft2
since this work was authored. Aligning with that revision - the removed
group vocabulary, the new projection/View construct, the remaining model
vocabulary terms and the two new portable-identity validation rules - is
tracked separately and will follow in its own pull request.

## Validation

Built for every target framework with zero warnings and zero errors, and
validated at the tip of the stack with `Opc.Ua.WotCon.Tests` at 999
passed / 0 failed and `Opc.Ua.Server.Tests` at 4024 passed / 0 failed.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9e6a5abf-3299-4cd1-9855-010fedbf0ad8
2026-08-05 10:14:15 +02:00
Marc Schier 00664404c0 Ship the OPC Foundation license file from every package and fix the NU5033 pack failure (#4174)
# Description

The `preview-pack` stage of the Azure DevOps pipeline fails with nine `NU5033` errors, which fails the whole build:

```
NuGet.Build.Tasks.Pack.targets(222,5): error NU5033: Invalid metadata.
Cannot specify both a PackageLicenseExpression and a PackageLicenseFile.
[src\Opc.Ua.Robotics\Opc.Ua.Robotics.csproj]
```

`common.props` gives every package the shared `LICENSE.txt` as its `<PackageLicenseFile>`, but **19** packages also declared `<PackageLicenseExpression>MIT</PackageLicenseExpression>`. Nine of them hit `NU5033`; the other ten only avoided it by carrying an empty `<PackageLicenseFile></PackageLicenseFile>` workaround.

That declaration was wrong on two counts.

**It advertised the wrong license.** `LICENSE.txt` is the **OPC Foundation MIT License 1.00**, which carries the OPC Foundation copyright and preamble — it is a different text than the plain SPDX `MIT` license. The affected packages therefore published `<license type="expression">MIT</license>` plus a `licenses.nuget.org/MIT` URL on nuget.org, instead of the OPC Foundation text that every other package in the stack ships.

**It also broke the build,** because NuGet refuses to pack a project carrying both a license expression and a license file, failing `pack` solution-wide rather than just for the offending projects.

## Changes

The expression is dropped from all 19 packages so every one uses the shared OPC Foundation license file uniformly, like the rest of the stack:

* `src/Opc.Ua.OpenUsd`, `.Client`, `.Server`
* `src/Opc.Ua.OpenUsdScene`, `.Server`
* `src/Opc.Ua.Robotics`, `.Client`, `.Server`
* `src/Opc.Ua.Positioning`, `.Client`, `.Server`
* `src/Opc.Ua.Di`, `src/Opc.Ua.ISA95`, `src/Opc.Ua.WotCon`, `src/Opc.Ua.Gds.Common`
* `tools/Opc.Ua.OpenUsd.Connector.Viewer`, `tools/Opc.Ua.SourceGeneration.Core`
* `tools/Opc.Ua.SourceGeneration.Pack`, `tools/Opc.Ua.SourceGeneration.Stack.Pack` — these two set the license not in their own `.csproj` but in the shared `tools/SourceGeneratorPack.targets`, so they were only caught once the new guard ran in CI.

The eight per-project empty `<PackageLicenseFile></PackageLicenseFile>` workarounds go away with it.

To stop the whole class of failure from recurring, `Directory.Build.targets` now *enforces* the rule rather than papering over it. Silently clearing the inherited license file would have fixed the build while still publishing the wrong license metadata, so instead a violation fails fast with an actionable error naming the project:

```
error OPCUA0001: Opc.Ua.Foo sets PackageLicenseExpression=MIT. Every package must
ship the OPC Foundation MIT License 1.00 via the shared LICENSE.txt that
common.props sets as PackageLicenseFile, which the SPDX expression neither
matches nor is allowed to coexist with (NU5033). Remove the
PackageLicenseExpression property.
```

`common.props` cannot catch this itself: it is imported from `Directory.Build.props`, i.e. **before** the project body, so it never observes a project-level license expression. `Directory.Build.targets` is imported **after** the project body and is the earliest place that can. The `common.props` comment that previously hinted the expression was an option now points at this rule.

### Verification

* Every packable project evaluates to `PackageLicenseFile=LICENSE.txt` with an empty `PackageLicenseExpression`; file-licensed packages such as `Opc.Ua.Client` are unchanged.
* `dotnet pack -c Release` succeeds for `Opc.Ua.Robotics` (previously failing) and `Opc.Ua.SourceGeneration.Core` (previously relying on the removed workaround).
* Replicated the CI build commands locally — `dotnet build` of `UA.slnx` (net48/Debug), `tools/SourceGeneration.slnx` (net472/Release) and `tools/MigrationAnalyzer.slnx` (net48/Debug) all succeed with 0 errors.
* Inspected the produced `.nupkg`: now `<license type="file">LICENSE.txt</license>` with the license text at the package root, matching the rest of the stack (it was `<license type="expression">MIT</license>` before).
* Confirmed the guard fires on a simulated violation (`-p:PackageLicenseExpression=MIT`) and is inert otherwise.

## Related Issues

No tracking issue — this fixes the `NU5033` failure in the OPC Foundation Azure DevOps pipeline (build 16247, `preview-pack` stage) and corrects the license metadata published for the affected packages. No product code or public API changes.

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step after opening the PR._

- [x] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc.
- [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. _(Build-infrastructure change with no code surface. Validated by `dotnet pack` of the affected projects, by inspecting the resulting package metadata, and by confirming the new `OPCUA0001` guard fires on a simulated violation. The pipeline `preview-pack` stage is the regression test.)_
- [x] I have added all necessary documentation. _(The rule is documented in place, on the new `Directory.Build.targets` guard and at the `PackageLicenseFile` definition in `common.props`.)_
- [x] I have verified that my changes do not introduce (new) build or analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and **all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-08-04 18:49:50 +00:00
Marc Schier 76d33cb139 Remove SourceGenerator.Foundations, ship analyzers under Roslyn-versioned folders for VS2022 and VS2026, and validate the packages (#4120)
# Description

The two OPC UA source generators were built on
`SourceGenerator.Foundations` (SGF), whose only job was getting the
generator's dependency closure loaded inside the compiler. It does so
with a **process-wide, first-generator-wins `AppDomain.AssemblyResolve`
hook backed by a non-concurrent `Dictionary` mutated from `AssemblyLoad`
callbacks**. Concurrent compilations in a shared `VBCSCompiler` race it,
which is the root cause of the intermittent clean-build failure:

```
CSC : error CS8784: Generator 'StackSourceGeneratorHoist' failed to initialize. ...
Exception was of type 'FileNotFoundException' with message 'Could not load file or assembly
'SourceGenerator.Foundations.Contracts, Version=2.0.14.0, ...'
```

The repository already solves that problem deterministically — #4041
made the NuGet packages ship the full closure under
`analyzers/dotnet/.../cs`, and `Directory.Build.targets` registers the
same closure as `Analyzer` items for in-repo `ProjectReference`
consumers. SGF was therefore pure liability and is removed.

## What changed

**1. SourceGenerator.Foundations removed.** `ModelSourceGenerator` and
`StackSourceGenerator` are plain `[Generator(LanguageNames.CSharp)]
IIncrementalGenerator` implementations again. SGF's implicit crash
handling is replaced by an explicit `SourceGenerator.Guard` that reports
`MODELGEN003` / `STACKGEN003` and rethrows cancellation. Its logger sink
was already dead code and is dropped.
`Opc.Ua.SourceGeneration.Stack.dll` shrank from **18 MB** (embedded
closure) to **29 KB**.

**2. Analyzers now ship under Roslyn-versioned folders.**
`analyzers/dotnet/roslyn4.14/cs` and `analyzers/dotnet/roslyn5.0/cs`
instead of `analyzers/dotnet/cs`, driven by `roslyn.props` at the
repository root, with new `*.Pack` projects owning the package identity.
The versioned folder matters: a host older than the band **skips** the
analyzer instead of loading it and failing inside generator
initialization.

**3. Dependencies flow automatically.** Both delivery paths discover the
payload by globbing the generator's output directory, so adding a
package to a generator's dependency tree requires **no** `.csproj`,
`.targets`, `.nuspec` or validation-script edit. Verified by temporarily
adding a `PackageReference` and watching it appear in the payload with
no other file touched.

## Two Roslyn bands: VS2022 and VS2026

Analyzers ship twice, once per band. The .NET SDK loads the highest band
its compiler supports and **silently skips** anything above it, so a
single-band package leaves every consumer below that band with no
diagnostics and no generated code.

| Band | Built against | Loaded by |
| --- | --- | --- |
| `roslyn4.14` | `Microsoft.CodeAnalysis.CSharp 4.14.0` | Visual Studio
2022 17.14+ / .NET 9 SDK |
| `roslyn5.0` | `Microsoft.CodeAnalysis.CSharp 5.0.0` | Visual Studio
2026 18.0+ / .NET 10 SDK |

Getting this wrong is invisible, which is most of why it took three
attempts. The rule:

> An analyzer must reference the **lowest**
`System.Collections.Immutable` / `System.Reflection.Metadata` across all
hosts it claims to support, and must never ship them. .NET binds a
reference *upward* but never downward.

Every failure mode is a compiler **warning**:

| Diagnostic | Cause |
| --- | --- |
| `CS9057` | Built against a newer Roslyn than the host, so it is
skipped entirely |
| `CS8784` | Loaded, then `MissingMethodException` - a shipped S.C.I
bound a second `ImmutableArray<T>` |
| `CS8032` | Analyzer instance could not be created |

This PR first shipped a `roslyn4.8` band that **never loaded** -
confirmed against a real Roslyn 4.8 compiler:

```
warning CS8784: Generator 'ModelSourceGenerator' failed to initialize ...
MissingMethodException: Method not found:
'IncrementalValueProvider`1<ImmutableArray`1<!!0>>
 IncrementalValueProviderExtensions.Collect(IncrementalValuesProvider`1<!!0>)'
```

That band was removed, and **this is not a regression**: master had the
same defect, pinned to Roslyn 4.14 while referencing
`System.Collections.Immutable 10.0.0.0`, so no 4.x host ever worked
there either.

The band that landed instead is `4.14`, and it is nearly free:
`Microsoft.CodeAnalysis 4.14` depends on the **same S.C.I 9.0.0 as
5.0.0** and its `csc` runs on `net9.0`. Both bands therefore share
`$(RoslynRuntimeVersion)`, so `Opc.Ua.Types` and
`Opc.Ua.SourceGeneration.Core` are compiled **once** and serve both;
only the five Roslyn-referencing assemblies get a second build, each
from a 12-line project that glob-links the primary sources and overrides
`RoslynApiVersion`. Going lower still - 4.8 wants S.C.I 7.x - would mean
rebuilding that whole closure, `Opc.Ua.Types` included, and remains out
of scope. (`System.Collections.Immutable 7.0.0` also does not resolve in
this repository: `NU1603`, 8.0.0 substituted.)

**Nothing else in the repository exercises the down-level payload**, and
since every failure is a warning, a broken band would ship silently -
exactly as the 4.8 one did. So `validate-source-generator-packages.ps1`
now restores a real `Microsoft.Net.Compilers.Toolset 4.14.0`, runs its
`csc.dll` over the packed `roslyn4.14` payload, and asserts **both**
that none of `CS9057`/`CS8784`/`CS8032`/`CS8034` is emitted **and** that
`/reportanalyzer` lists the generator as having executed - silence alone
would also pass if the generator were never handed to the compiler. The
migration analyzer, whose payload comes from a hand-written nuspec
rather than `SourceGeneratorPack.targets`, gets the same check plus a
band/assembly manifest assertion.

To stop the trap recurring, `Microsoft.CodeAnalysis*`,
`System.Collections.Immutable` and `System.Reflection.Metadata` are
excluded from both the package payload and the in-repo analyzer closure,
and asserted against in the validation script. `roslyn.props` and
`docs/DeveloperGuide.md` document what adding a band requires.
## The shipped package set is unchanged

Evaluating `PackageId`/`IsPackable` across every project in
`preview-pack.slnx` gives the **same 47 package ids** on `master` and on
this branch, and a full Release build plus `dotnet pack --no-build`
produces exactly those 47. Only the owning project changed:

| Package | master | this PR |
| --- | --- | --- |
| `…Opc.Ua.SourceGeneration` | `Opc.Ua.SourceGeneration.csproj` |
`Opc.Ua.SourceGeneration.Pack.csproj` |
| `…Opc.Ua.SourceGeneration.Stack` |
`Opc.Ua.SourceGeneration.Stack.csproj` |
`Opc.Ua.SourceGeneration.Stack.Pack.csproj` |

`.azurepipelines/expected-packages.txt` pins the expected set and the
validation script fails when the packed output drifts.

## Two new package checks

**`build/<PackageId>.props` naming.** NuGet only auto-imports that exact
name, and the file declares every `CompilerVisibleProperty` /
`CompilerVisibleItemMetadata` the model generator relies on. It was
previously packed as
`build/OPCFoundation.Opc.Ua.SourceGeneration.props`, which does **not**
match the package id — so **no NuGet consumer ever imported it**, and
every `ModelSourceGenerator*` property and `AdditionalFiles` metadata
was silently invisible. In-repo projects were unaffected because they
import it by relative path.

**An end-to-end consumer that actually drives the generator.** The
pre-existing clean-consumer check only proved the analyzer *loads*.
`Test-SourceGeneratingConsumer` builds a standalone project that feeds a
real NodeSet2 in as an `AdditionalFile`, pins a
`ModelSourceGeneratorPrefix` that cannot be derived from the model, and
references the emitted types from hand-written code. It deliberately
does **not** import the props by path, so the package's auto-import is
what is under test. Both checks were verified to fail against a package
rebuilt with the misnamed props.

## CI: parallelism re-enabled

`buildandtest.yml` pinned three `dotnet build` invocations to
`/p:UseSharedCompilation=false` and `-maxcpucount:1`. `dotnet build`
defaults to all logical processors, so `-maxcpucount:1` actively
disabled parallelism. Both flags were introduced silently in unrelated
PRs (`f358078b1`, `208c601b0`) and suppress exactly the conditions the
SGF race needed — now structurally gone. Compilation is ~72 % of what
that workflow executes (233 of 323 job-minutes in run `29916768976`), so
the queue-independent `Build` step durations are the baseline:

| Measurement | Baseline |
| --- | --- |
| `Build` step, 26 `test-*` jobs | mean 5.53 min, sum 143.9 min |
| `build-linux-all-tfm` | 56.3 min |
| `build-windows-all-tfm` | 32.5 min |

`codeql-analysis.yml` keeps the flag — the CodeQL tracer requires it.

## Incidental fixes

- `Microsoft.CodeAnalysis.CSharp` declares an exact `[version]` range on
`Microsoft.CodeAnalysis.Common`, so with
`CentralPackageTransitivePinningEnabled` the central pins must move in
lockstep or restore fails with `NU1109`. They now derive from
`$(RoslynApiVersionVS2026)`.
- `MigrationGeneratorTests` created its generator driver without parse
options — silently fine under Roslyn 4.14, `Inconsistent language
versions` under 5.0.
- The migration analyzer props injected `<Analyzer>` items by hand from
`analyzers/dotnet/cs`; NuGet registers analyzers itself and selects the
matching folder, so the injection was obsolete and would now force the
wrong assembly to load.
- Exception diagnostics reported `ex.StackTrace` (null for an exception
that never left its throw site, and carrying neither type nor inner
chain) — now `ex.ToString()` at all three call sites.
- The pack guard tested the flattened assembly list, so it only fired
when *no* variant produced output; it now checks each declared variant
contributed assemblies.
- `GeneratePackageOnBuild` is disabled for the generators; packing
happens in CI or on an explicit `dotnet pack`.

## Note for reviewers

**Building this repository requires a Roslyn 5.x host** (the .NET 10 SDK
or Visual Studio 2026). That is already the documented prerequisite and
every CI leg installs `10.0.x` last, but it is now a hard requirement.

## Related Issues

_No tracking issue: this is a follow-up to the CS8784 clean-build
failure reported against #4041, which fixed the packaged delivery path
but left the in-repo project-reference path relying on the SGF
resolver._

- Follow-up to #4041

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [x] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.

### Verification detail

| Check | Result |
| --- | --- |
| `dotnet build UA.slnx` (clean, and after merging master) | 0 errors;
18 warnings, all pre-existing `CA1873` in `Opc.Ua.Client` |
| `Opc.Ua.SourceGeneration.Tests` | 81/81 on net10.0 and net48 |
| `Opc.Ua.SourceGeneration.Stack.Tests` | 92/92 on net10.0, 89/89 on
net48 |
| `Opc.Ua.SourceGeneration.Core.Tests` | 3722 passed, 8 skipped |
| `Opc.Ua.MigrationAnalyzer.Tests` | 121/121 on net10.0 and net48 |
| Package id set, master vs branch | identical, 47 packages |
| Release build + `dotnet pack --no-build` | 47 packages, matching the
manifest; both `roslyn4.14` and `roslyn5.0` present; no host-provided
assemblies shipped |
| `validate-source-generator-packages.ps1` | passes, including manifest,
props-naming, end-to-end generator consumer and down-level host checks |
| Down-level band under real csc 4.14.0 | all three analyzer packages
load **and run** |
| Negative tests | misnamed props → both checks fail as designed; hidden
variant output → pack guard fires; 5.0 build placed in `roslyn4.14` →
real `CS9057`; non-running generator name → execution assertion fires;
expecting a `roslyn4.8` band → manifest check fires |

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 9ed53262-5cfa-429a-820e-bf6146a00d8b
2026-07-31 15:46:17 +00:00
Marc Schier f0ab7878ea Core stack: reusable file directory binder, geolocation seam and state machines (#4123)
**Stack 1/5** — base `master`. Part of the #4096 split; see #4096 for
the overall picture.

## What this is

The core stack changes the Robotics, OpenUSD and Positioning work
depends on. No new projects; every change is to an existing one, so this
is reviewable without any of the companion specs.

## Contents

**`Opc.Ua.Server`**
- `FileSystem` gains a reusable `IFileDirectoryBinder` and
`IFileSystemHost`, so any node manager can serve a Part 5
`FileDirectoryType` over `IFileSystemProvider` instead of that binding
living inside one companion model.
- Entry names are validated before they reach the provider. The contract
promises forward-slash provider-relative paths but never enforced it, so
a backslash payload such as `..\..\..\Windows\System32\evil.txt`
bypassed the obvious provider-side check on Windows. Defence in depth —
the shipped `PhysicalFileSystemProvider` was already safe.
- `Location` adds a single `IGeoLocationProvider` seam so one provider
implementation serves both OPC 10000-211 GPOS and OPC 10030 ISA-95. The
two specs disagree on representation, not on content, so the difference
is a projection rather than a reason to write the provider twice. Ships
a WKT text formatter and an in-memory provider.
- `StateMachines` adds `FiniteStateMachineDispatcher`, the Part 16
transition guard used by DI software update and later by Robotics task
control.

**`Opc.Ua.Types`**
- Node copies no longer consume NodeIds from the `NodeIdFactory`.
`Initialize` materialises children through `CreateChild`, which assigns
a per-instance NodeId, but the copy then overwrites every child NodeId
from its source, so the assignment is discarded. A counting factory
silently burned identifiers; a tracking one leaked them.
- `JsonDecoder` resolves a relative encodeable type id against the
namespace table before the factory lookup.

**`Opc.Ua.Client`** — a file system path segment written without a
namespace prefix is qualified with the namespace of the parent it is
resolved against, matching the provider-owned BrowseNames.

**`Directory.Build.targets`** — the `RestrictForLegacyTfm` no-op now
also applies when a project pins a single `TargetFramework` and
`CustomTestTarget` names a different one. That is the same situation it
already handled for the legacy frameworks, and it is what keeps the
per-TFM CI matrix green once net10.0-only projects appear later in the
stack.

## Validation

`dotnet build UA.slnx` — 0 errors.

| Suite | Result |
| --- | --- |
| Opc.Ua.Types.Tests | 8088 / 0 |
| Opc.Ua.Server.Tests | 3873 / 1 |
| Opc.Ua.Client.Tests | 2105 / 0 |
| Opc.Ua.Core.Tests | 4145 / 0 |
| Opc.Ua.Features.Tests | 134 / 0 |

The single `Opc.Ua.Server.Tests` failure is
`ConfigureApplicationBuildsSharedClientAndServerConfigurationAsync`,
which is pre-existing and environmental: the PKI path in my working
directory exceeds MAX_PATH. It reproduces on clean `master` and passes
from a shorter directory.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
2026-07-31 13:13:47 +02:00
Marc Schier 7d0e5870cb Migrate to source-generated logging across Stack, Libraries and Applications (#3908) (#4003)
# Description

Migrates the entire code base (Stack, Libraries and Applications) from
direct

`ILogger.LogInformation/LogError/LogWarning/LogDebug/LogTrace/LogCritical(...)`
calls to **source-generated `[LoggerMessage]` logging**, as tracked in
#3908.
Source-generated logging avoids boxing of value-type arguments, caches
the
message formatter, and emits an efficient `IsEnabled` check so a
disabled log
level costs nothing.

This is a behavior-preserving refactor: message text, levels and
structured
placeholders are unchanged; only the call shape changes.

## What changed

- **36 projects migrated.** Every static log call now goes through a
per-file
  `internal static partial class <Class>Log` holding `[LoggerMessage]`
  extension methods on `ILogger`, so call sites keep the natural
`logger.SomeEvent(...)` shape. Closely-related files that emit the same
messages share one log class (e.g. the encoders/decoders in
`Opc.Ua.Types`
  share `EncodingLog`) to avoid duplication and CS0121 ambiguity.
- **Per-assembly event-id classes.** Each project has one
  `internal static class <AssemblyToken>EventIds` (e.g. `CoreEventIds`,
  `ClientEventIds`, `PubSubEventIds`) with a reserved `public const int`
  offset block per log class. The assembly-token prefix avoids CS0436
  collisions across `InternalsVisibleTo` boundaries.
- **`CA1873` enabled solution-wide** in `.editorconfig` now that the
whole
code base (including test, sample and tooling projects) is migrated, so
any
regression back to a direct `ILogger.Log*` call with an expensive
argument is
flagged. Every occurrence has been guarded with an `IsEnabled` check (or
the
check merged into an existing condition where the log was the sole body
of an
`if`/`else`, preserving side effects); the only suppressions are two Moq
  `Verify` expression-tree sites, which are not executed log calls.
- **Dynamic log levels stay hand-written.** A handful of calls whose
level is
only known at runtime keep the structured `logger.Log(logLevel, ...)`
form
under an `if (logger.IsEnabled(logLevel))` guard — `[LoggerMessage]`
requires
  a compile-time level.
- **netstandard duplicate-generator fix.** Projects that also pull in an
R9
package (`Microsoft.Extensions.Http.Resilience`, `.Compliance`,
`.Telemetry`)
get the `Microsoft.Gen.Logging` generator in addition to the in-box one;
on
  `netstandard` both implement every partial method (CS0757). A single
`Directory.Build.targets` target removes the R9 analyzer on
`netstandard`
  only (no-op elsewhere and where it is not referenced).
- **Shared/linked source files** that are compiled into more than one
project
(e.g. sample files linked into test projects) use self-contained literal
event ids instead of a per-assembly class the consumer does not compile.
- **Docs.** The authoring recipe (event-id convention, log-class
convention,
  parameter rules, the `IsEnabled` guard, shared/linked files, and the
  netstandard generator interaction) lives in the new

[`Docs/DeveloperGuide.md`](Docs/DeveloperGuide.md#add-a-log-message-source-generated);
`Docs/Diagnostics.md` keeps the runtime/observability side and links to
it.
- **Redaction preserved.** Log calls that previously wrapped sensitive
values
with `Redact.Create(...)` keep passing the `RedactionWrapper<T>`
(instead of
an eager `.ToString()`), so format-time redaction still applies — this
also
  clears the CodeQL clear-text alerts introduced during the migration.

## Validation

- Full `UA.slnx` build passes with **0 errors / 0 warnings**; the
multi-target
  test projects force every referenced library to build on
  net472/net48/netstandard2.1/net8.0/net9.0/net10.0.
- No unguarded static `ILogger.Log*` calls remain anywhere (only the
guarded
dynamic-level calls); `CA1873` is clean across the full solution on
net10.0.

## Related Issues

- Fixes #3908

## Checklist

_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [ ] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-07-15 15:07:25 +02:00
Marc Schier 7f409657ad OPC UA Part 4 6.6 Redundancy (server + client) with opt-in distributed high-availability (#3918)
## Summary

This branch implements **OPC 10000-4 §6.6 Redundancy** end to end —
server and client, **transparent and non-transparent**, across all
failover modes (`None`, `Cold`, `Warm`, `Hot`, `HotAndMirrored`,
`Transparent`) — and layers an **opt-in distributed high-availability
(HA)** capability on top so replicas can share address-space, session,
and subscription state and expose redundancy to clients through the
documented OPC UA mechanisms (for example, running a server replica set
across nodes in Kubernetes).

The single-instance, in-memory path stays **zero-overhead**: every
distributed feature is opt-in through dependency injection, with a
direct-construction fallback, and replaced APIs are kept and marked
`[Obsolete]`.

## New packages

| Package | Purpose |
| --- | --- |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy` | §6.6 redundancy
nodes plus opt-in distributed state: shared address space, secure
session sharing, subscription/continuation-point mirroring, and leader
election. |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy.Crdt` |
Active/active, leaderless multi-writer replication via CRDTs (an
extension beyond §6.6). |
| `OPCFoundation.NetStandard.Opc.Ua.Server.Redundancy.K8s` | Kubernetes
integration: Lease leader election, EndpointSlice peer discovery, and
ServiceLevel-driven readiness. |

Also adds the worked samples `Applications/RedundantServer` and
`Applications/RedundantClient`, and the guides
`Docs/HighAvailability.md` and `Docs/HighAvailabilityKubernetes.md`.

## Server side (§6.6)

- `AddServerRedundancy(...)` populates the live
`Server.ServerRedundancy` nodes (`RedundancySupport` plus
`RedundantServerArray` / `ServerUriArray` / `CurrentServerId`) and
drives `Server.ServiceLevel` from an `IServiceLevelProvider` (leader
high, standby low). `Server.ServerRedundancy` is typed to the
NonTransparent or Transparent subtype according to the configured mode.
- `AddRequestServerStateChange(...)` implements the §6.6.5
manual-failover method, with `EstimatedReturnTime` and the Maintenance /
NoData `ServiceLevel` sub-ranges.
- Non-transparent peers resolve through `FindServers`
(`ConfiguredRedundantServerSetProvider`) and advertise the `NTRS`
discovery capability for GDS/NTRS registration.
- Deterministic, replica-stable `EventId` synchronization for
Transparent and HotAndMirrored sets (§6.6.2.2), excluding per-replica
fields so de-duplicating clients do not double-process events.

## Client side (§6.6)

- `DefaultServerRedundancyHandler` reads `Server.ServerRedundancy` and
`Server.ServiceLevel` and fails over to the highest-ServiceLevel peer.
It honors Maintenance (Part 4 Table 105 — disconnect to an available
peer), keeps Warm backups disabled until failover (Table 107), and
treats peers known only through `ServerUriArray` as failover candidates.
- `RedundantManagedClient` implements the Cold, Warm, Hot (a), Hot (b),
and HotAndMirrored client patterns: one active session, optional
lightweight ServiceLevel status-check sessions to backups, and — for Hot
(b) — merging of concurrent reporting streams with exact value-identity
de-duplication. HotAndMirrored failover re-activates the mirrored
session with the existing `AuthenticationToken` instead of recreating
it.
- Network redundancy: alternate communication paths via endpoint
selection.

## Distributed state (opt-in; an extension beyond §6.6)

- `ISharedKeyValueStore` (in-memory default) and `INodeStateStore`, with
an `AddressSpaceSynchronizer` that bridges a `CustomNodeManager2`'s
predefined nodes to the shared store, so node and reference
additions/removals and value changes propagate to other replicas.
`IDistributedValueCache` lets read/write callbacks cache the last value
with a freshness bound; monitored items use the normal read pipeline and
therefore participate only when the read path does.
- **Secure session sharing (active/passive fast reconnect), server and
client.** `DistributedSessionManager` (wired through a new additive
`ISessionManagerFactory` seam on `StandardServer`) mirrors **encrypted**
session state across replicas. On a failover reconnect the standby
restores the session and performs the **full `ActivateSession`
client-certificate signature validation** against a **single-use**
`serverNonce` consumed via compare-and-swap across the replica set,
enforcing the same SecurityPolicy and SecurityMode. The
`AuthenticationToken` is a **lookup key only**, never an authenticator:
entries are keyed by the SHA-256 digest of the token, and a
cross-replica restore emits a distinct audit event.
- Subscription, retransmission-queue, and continuation-point mirroring
for subscription transfer and failover.
- Leader election: a static single-leader provider and a shared-store
**lease via compare-and-swap**, supporting active/passive and
active/active (shared-read / master-write).
- Fail-closed security defaults: shared records are protected at rest
through `IRecordProtector`; secret-bearing mirrors require a registered
protector or an explicit opt-out.

## Active/active (CRDT)

`Opc.Ua.Server.Redundancy.Crdt` adds leaderless multi-writer
address-space replication with CRDTs and gossip
(`UseReplicatedAddressSpace`) plus CRDT-backed session metadata
(`UseReplicatedSessions`). Networked gossip is **fail-closed without
authenticated transport** (mutual TLS for TCP; an explicit opt-out for
isolated dev/test). CRDT state is eventually consistent, so exactly-once
decisions (such as the single-use nonce registry) stay on a strongly
consistent store. The package is available on all stack target
frameworks.

## Kubernetes

`Opc.Ua.Server.Redundancy.K8s` provides Kubernetes Lease leader
election, EndpointSlice-based peer discovery, and ServiceLevel-driven
readiness for running an OPC UA server replica set.
`Docs/HighAvailabilityKubernetes.md` covers StatefulSet/Deployment and
Service manifests, RBAC, probes, time synchronization, secrets,
gossip-port NetworkPolicy, and GDS/NTRS registration.

## Compatibility and validation

- **Zero-overhead** single-instance default; additive,
backward-compatible seams (for example `ISessionManagerFactory` and
`IServerStartupTask`); replaced APIs marked `[Obsolete]` for migration.
- Builds across the full stack TFM set (`net472`, `net48`,
`netstandard2.0`, `netstandard2.1`, `net8.0`, `net9.0`, `net10.0`);
NativeAOT-compatible where the runtime supports it.
- Unit and integration tests for server, client, subscriptions, session
mirroring, CRDT, and Kubernetes; the conformance suite stays green.

See `Docs/HighAvailability.md` for the full OPC 10000-4 §6.6 mapping,
the `Add*` (standard nodes) versus `Use*` (extension) builder
convention, and the security/rotation guidance.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-07-11 04:47:25 +02:00
Marc Schier 4d613d11dc CI: migrate to netstandard Managed DevOps Pool + per-TFM build matrix (#3866)
# Description

End-to-end migration of the Azure DevOps CI pipelines to the
`netstandard` Managed DevOps Pool with the build matrix re-shaped to fit
the new agents and a handful of test/project fixes that fell out of the
migration.

**Pipeline routing & defaults**

- `azure-pipelines.yml` and `azure-pipelines-preview.yml` default
`managedPool=true`, routing Windows/Linux jobs to the `netstandard`
Managed DevOps Pool with `ImageOverride` demands; flipping the
queue-time boolean to `false` falls back to Microsoft-hosted agents.
- Per-platform images come from the root yml (single source of truth):
- Managed pool: `windows-2022-g2` / `ubuntu-22.04-g2` (Mac stays hosted:
`macOS-15`).
  - Hosted: `windows-2025-vs2026` / `ubuntu-24.04` / `macOS-15`.
- `.azurepipelines/test.yml` and `test-aot.yml` declare `hosted` as a
typed `boolean` parameter so `not(parameters.hosted)` actually evaluates
correctly (an untyped value left it as the string `'false'`, which is
truthy and silently routed managed-pool jobs to Microsoft-hosted
`vmImage` demands).
- macOS test jobs always run on the Microsoft-hosted `macOS-15` image
regardless of `managedPool`; the four per-framework test stages queue a
dedicated `hosted: true` Mac invocation unconditionally and the main
matrix carries only Windows+Linux.
- Test-template prep jobs (which only run a PowerShell matrix script)
run on the managed pool whenever a `poolName` is set, even when the
matrix `hosted: true`, so the Microsoft-hosted parallel-job allotment
isn't blocked.

**Build stage — per-(TFM × configuration) matrix**

- `.azurepipelines/get-matrix.ps1` gained a `-Tfms` parameter that fans
the matrix across target frameworks alongside the existing agent /
configuration dimensions and emits a per-entry `targetTfm`.
- `.azurepipelines/sln.yml` accepts a `tfms` parameter and threads
`$(targetTfm)` into both `dotnet restore` and `dotnet build` as
`/p:CustomTestTarget=$(targetTfm)` so each matrix job builds a single
TFM and fits on the Managed DevOps Pool's small (~16 GB) temp disk.
- `azure-pipelines.yml` `build` stage passes `tfms: 'net10.0,net48'` for
PR builds and the full
`net472,net48,netstandard2.0,netstandard2.1,net8.0,net9.0,net10.0` set
for master / scheduled / manual / fork-PR runs.
- `sln.yml`'s restore step uses `command: custom` (`custom: restore`)
because `command: restore` silently drops the `arguments` input —
without it `/p:CustomTestTarget` never reached restore and
`obj/project.assets.json` was populated with the wrong TFM set.
- `sln.yml` no longer uses the `MSBuild@1` task; it now uses
`DotNetCoreCLI@2` with `dotnet build` so the .NET 10 SDK's bundled
MSBuild (which understands `.slnx`) is used instead of Visual Studio
2022's MSBuild on the managed-pool image.

**Project-level fixes that fell out of the matrix split**

- `Applications/McpServer`, `Applications/MinimalBoilerServer`,
`Applications/MinimalCalcServer`, and `Tests/Opc.Ua.Aot.Tests` each get
a `Directory.Build.targets` that turns the project into a no-op (cleared
`Compile` / `ProjectReference` / `PackageReference` / etc.,
`OutputType=Library`) when `CustomTestTarget` restricts the matrix to a
legacy TFM (`net4*` / `netstandard2.*`) the project can't target. Target
overrides are deliberately avoided — same-named targets in
`Directory.Build.targets` ALWAYS replace the SDK definitions, which
would silently break `dotnet publish` / `dotnet test` in the
unrestricted GitHub Actions flows.
- `Tools/Opc.Ua.MigrationAnalyzer.csproj`'s `ProjectReference` to
`Opc.Ua.MigrationAnalyzer.Core` no longer hard-pins
`SetTargetFramework=net10.0`; the pin follows `CustomTestTarget` (and
falls back to `net10.0` when unset).

**Test reliability**

-
`Tests/Opc.Ua.PubSub.Tests/IntervalRunnerTests.FakeTimeProviderDrivesDeterministicSchedulingAsync`
no longer calls a single `fake.Advance(300)` expecting three timers to
fire — it advances one 100 ms interval at a time and waits for each fire
(the runner only registers the next `Delay` after the previous callback
resumes on the thread pool; on slower CI agents the race surfaces
consistently).
- `Tests/Opc.Ua.Server.Tests/Fluent/SimulationBuilderExtensionsTests`
(four "expect ≥ N ticks in 150 ms" tests) replace fixed
`Task.Delay`/`Thread.Sleep` waits with a `WaitForAsync` polling helper,
eliminating thread-pool cold-start flakiness on slow agents (observed on
the hosted `macOS-15` runner).

## Related Issues

_No tracking issue — pipeline tuning to standardize on the netstandard
Managed DevOps Pool, plus the test/project follow-ups required for the
matrix to be green on that pool._

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage. _(N/A — pipeline +
test-reliability fixes; the two test fixes themselves were verified to
pass 5× in stress runs locally.)_
- [x] I have added all necessary documentation. _(Comments inline in the
new `Directory.Build.targets`, `sln.yml`, and `azure-pipelines.yml`
explain the unusual MSBuild/`DotNetCoreCLI@2` interactions for future
maintainers.)_
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [x] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed. _(Verified
`dotnet build UA.slnx -p:CustomTestTarget=net10.0 -c Release` and
`-p:CustomTestTarget=net48 -c Debug` both complete with 0 errors;
targeted test runs for the two modified test files pass 5/5.)_
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [ ] I have addressed **all** PR feedback received.
2026-06-10 17:19:41 +02:00