Commit Graph

7 Commits

Author SHA1 Message Date
romanett b8f07843b2 Secure channel: never answer with an expired token; accept a just-expired one (+ labelled soak/stress CI) (#4698)
## Summary

**Secure channel: a client could drop the channel when the server
answered with a just-expired token.** Running the long-dormant
ConnectionStability soak locally, the subscription channel shut down
with `BadSecureChannelClosed`. The session then timed out and the
subscription was abandoned. Root cause, against OPC 10000-4 §5.6.2.1:

1. **Server (rule 2: use the old token only "until the SecurityToken
expires").** After a renew, the server keeps securing messages with the
old token until the client sends something secured with the new one.
That activation only happened on *receive*. A client that sends nothing,
for example one whose Publish requests are all outstanding, kept getting
responses secured with the old token after it expired.
2. **Client (rule 3: "accept Messages secured by an expired
SecurityToken for up to 25 % of the token lifetime").** The client
rejected any such response the moment the token expired, throwing
`BadTcpSecureChannelUnknown`, and shut the channel down.

From the failing run (token lifetime 10 s): the client renewed to token
#11 at 09:59:01.686 and sent nothing on that channel afterwards. The
server, still on #10, sent a Publish response at 09:59:04.389, 380 ms
after #10 expired. The client rejected it and closed the channel.
Nothing reconnected, the 10 s session timeout expired, and the server
abandoned the subscription.

Both halves date back to the 2016 initial commit; this is not a recent
regression. It hits whenever the client sends nothing on the channel
between renewing and the old token's expiry, so it is timing-dependent.

### Fix
- `UaSCBinaryChannel.ActivateRenewedTokenIfDue()` activates a pending
renewed token once the current one passes `TokenActivationPeriod` (95
%). This is the rule the receive path already applied. It is now shared,
and `TcpServerChannel.SendResponse` calls it before securing a response.
- `ReadSymmetricMessage` accepts responses (not requests) secured with
an expired token for `TcpMessageLimits.TokenExpiryGracePeriod` (25 %) of
its lifetime.

### Tests
`TcpReconnectKeyAgreementTests` now runs the server channel and the peer
on one shared `FakeTimeProvider`:
- `ResponseSentAfterTheCurrentTokenExpiresUsesTheRenewedTokenAsync`: a
response sent at 96 % of the old token's lifetime is secured with the
renewed token.
-
`ResponseSecuredWithTheExpiredTokenIsAcceptedWithinTheGracePeriodAsync`:
accepted at 120 % of the lifetime, rejected at 130 %.

Both fail without the fix. All 40 tests in the fixture pass with it.

## CI: soak and stress runs on labelled PRs (and the soak that tested
nothing)

The bug stayed hidden because `stability-test.yml` still filtered
`Opc.Ua.Client.Tests` after #3810 moved the test to
`Opc.Ua.Sessions.Tests` (2026-05-28). Since then the nightly has run
**zero tests** while reporting green: runs dropped from ~100 min to ~15
min.

- `stability-test.yml` now targets `Opc.Ua.Sessions.Tests` and writes a
TRX file.
- New `.github/scripts/assert-tests-ran.ps1`, used by all three opt-in
workflows, fails a run in which no test executed, or none passed.
Contract-tested in `CiAssertTestsRanTests`.
- Like `interop`, the opt-in workflows can now run on a labelled PR.
They are not required checks. A later push cancels the run in progress,
and adding an unrelated label neither starts nor cancels a run.

| Label | Workflows | Run on a PR |
| --- | --- | --- |
| `stress` | Stress Tests (ChaosTCP) | Random seed, saved as
`chaos-seed.txt` in the results artifact |
| `soak` | Connection Stability Test, Sample HA Long-Haul Test | 30 min
stability; 15 min per HA scenario |

## Test plan

- [x] `TcpReconnectKeyAgreementTests`: 40/40 pass; the 2 new tests fail
without the fix
- [x] All `Opc.Ua.Core.Tests.Stack` tests (net10.0): 3548 passed, 0
failed
- [x] `CiAssertTestsRanTests`: 4/4
- [x] `soak` + `stress` labelled run on this PR (before the fix): both
stability tests, both HA long-haul scenarios and ChaosTCP passed on
Linux (the label triggers and push-cancellation work)
- [x] Local short-haul soak with the fix (Windows, 10 s tokens), 2 runs:
both pass with full notification counts; run 2 hit the window once
(`Token activated forced` on send) and kept the channel
- [x] `soak` + `stress` labelled run with the fix: Connection Stability
(2 tests), Sample HA Long-Haul and ChaosTCP all pass

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:14:25 +02:00
romanett fbaabf5b28 Cut CI runner minutes: duration-packed test batches, CodeQL on Linux/daily, cancellable branch packing (#4694)
Reduces GitHub Actions runner minutes without removing any OS, target
framework, test project or check. All figures come from master CI run
37520359929 (158 jobs, ~1,880 runner-minutes) and the last two weeks of
run history.

## 1. Test batches packed by duration (`get-ci-matrix.ps1`)

Previously the expander picked the **smallest** batch size that fit the
256-job limit, which meant 2 projects per job and 33 jobs per profile.
Job logs show ~45% of the test matrix (674 of ~1,516 min) went to
compiling. Each batch rebuilds the shared dependency graph: the first
project's build took 3.8–4.9 min, the second only 0.4–0.9 min.

- Projects are now packed in path order (which keeps `PubSub.*`,
`Redundancy.*`, `WotCon.*` together) up to a 30-minute weight target,
using `.github/ci-test-durations.json`. Each weight is the slowest PR
profile's test minutes, rounded up, plus 1.
- A batch never holds more projects than the job timeout can budget for:
at most 7, given 20 + n×45 ≤ 350.
- If the matrix would exceed the job limit, the target is raised
instead.
- **PR test matrix: 132 → 60 jobs** (15 per profile; macOS 33 → 15).
Each batch is estimated at ≤36 min on the slowest runner, comparable to
today's slowest jobs (37–43 min), so wall-clock should be about the
same. Expected saving is roughly 300+ runner-min per run, plus less
queueing for the scarce macOS runners.
- The weekly full scope shrinks the same way (still 13 profiles).
- The table only steers balance: a project missing from it gets
`defaultMinutes`, so it is still scheduled.
- `run-dotnet-tests.ps1` now records per-project
`buildSeconds`/`testSeconds` (batch summary and job summary). The new
`update-test-durations.ps1` refreshes the table from any run's
`dotnet-results-*` artifacts.
- `discover` output `batch_size` is renamed to `batch_target_minutes`.
Nothing consumed it.

New `CiMatrixScriptTests` cases:
- each profile runs each of its projects exactly once;
- only a lone project may exceed the target;
- PR profiles average ≥3 projects per job;
- the duration table names only existing projects.

## 2 + 3. CodeQL (`codeql-analysis.yml`)

- **No longer runs on every push to master.** Instead, a daily schedule
on master (was weekly) gives an analysis that always completes. Each
analysis takes ~3 h, and with `cancel-in-progress: true` per branch, 49
of 76 push runs in two weeks were cancelled by the next merge before
uploading anything. `master378`/`release/*`/`develop/*` keep their push
trigger. PRs are still analyzed as before.
- Only PR runs are cancelled when superseded. Push and scheduled runs
always finish.
- **Runs on `ubuntu-latest` instead of `windows-2025`.** Only net10.0 is
built, so the extracted sources are identical. The traced Windows build
alone took 70 min, against 12 min for the untraced Linux `UA.slnx`
net10.0 leg. This PR touches a `.cs` file, so its own CodeQL run is the
first measurement.

## 4. NuGet packing on feature branches (`nuget-publish.yml`)

298 of ~400 runs in two weeks were feature-branch pushes, each about 59
Windows-min across two signed builds.

- A superseded feature-branch run is now cancelled. Protected lines and
tags are never cancelled: they publish previews, and `release.yml`
promotes a run by ID. Each manual (`workflow_dispatch`) run gets its own
concurrency group, so a later push cannot cancel it.
- A new `changes` job skips packing when a feature branch, as a whole,
changes only documentation. It diffs against the merge base with master,
not the previous push, so a docs-only push that cancels a code push
cannot mask that code change. It uses the same `get-path-relevance.ps1`
rule as CI.
- Protected refs, tags, manual runs, and any branch whose merge base
cannot be resolved always pack. Checked locally for docs-only branch
(skip), code branch, protected ref, manual run and unknown commit (all
pack).

## Verification

- `CiMatrixScriptTests`, `CiPathRelevanceTests` and the new
`CiTestDurationsScriptTests` (covering `update-test-durations.ps1`): all
`*.Ci*` tests in `Opc.Ua.Tools.Tests` pass locally, 63/63.
- This PR's own CI run shows the new matrix. Compare its total
runner-minutes and longest test job against run 37520359929.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 10:00:19 +02:00
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
romanett d623007a3f Remove the Azure Pipelines CI and publishing definitions from master (#4575)
# Description

Removes the Azure Pipelines CI and package-publishing definitions from
`master`. GitHub Actions already owns all CI and package publishing for
`master`, but pushes to `master` still queued the Azure definitions.
They duplicated the Actions work and posted failing checks, for example
[`Nuget Preview (Push Nuget Preview Pack Nugets
Debug)`](https://github.com/OPCFoundation/UA-.NETStandard/runs/109065914524)
on `19dc504c5`.

## Removed

- `azure-pipelines.yml`, `azure-pipelines-preview.yml`
- `.azurepipelines/` templates: `ci.yml`, `coverage.yml`, `preview.yml`,
`sample-ha-longhaul.yml`, `sln.yml`, `test.yml`, `test-aot.yml`
- Helpers only those templates used: `get-matrix.ps1`, `get-root.ps1`,
`evaluate-test-results.ps1`
- `TrxGateScriptTests`: it tested only `evaluate-test-results.ps1`. The
Actions equivalent, `.github/scripts/get-test-verdict.ps1`, keeps its
own `CiTestVerdictTests`.

## Kept

**OneFuzz:** `.azurepipelines/onefuzz.yml` stays. Submitting to the
internal OneFuzz service needs the Azure DevOps-only `onefuzz-task`,
which has no GitHub Actions equivalent. The pipeline has no push, PR or
schedule trigger, so it never runs on `master` pushes. The public drop
build keeps running in the GitHub `fuzz-public-drop` job.

**Scripts GitHub Actions still invokes:** these stay in
`.azurepipelines/`, so every workflow path and `release.yml`'s
candidate-bound checkout keep working. Only `generate-slnx.ps1` changes,
and only its comment.

| Script | Used by |
| --- | --- |
| `set-version.ps1` | buildandtest, nightly, nuget-publish,
docker-image, stress / stability / long-haul, `run-dotnet-tests` action
|
| `generate-slnx.ps1`, `signlist{Debug,Release}.txt`,
`validate-source-generator-packages.ps1` (+ `expected-packages.txt`) |
nuget-publish |
| `package-version-policy.ps1`, `validate-nuget-package-set.ps1`,
`validate-preview-package-ordering.ps1`,
`assert-published-packages-match.ps1` | nuget-publish, release |
| `check-coverage.ps1` | buildandtest, nightly |
| `validate-migration-plugin.ps1` | buildandtest |

I didn't rename the directory on purpose. `release.yml` checks out the
candidate commit and runs `./.azurepipelines/package-version-policy.ps1`
from it, so moving the scripts would break promotion of any candidate
built before the move.

## Docs and comments

- `UA.slnx` drops the deleted files.
- `codecov.yml`, `coverage-thresholds.json`, `targets.props`, the
Redundancy sample test comments, the `.github` scripts and workflows,
`CONTRIBUTING.md`, `docs/` and `fuzzing/` no longer describe Azure as
running on `master` (`/azp run`, the `azure` Codecov flag, "the Azure
gate", the Azure long-haul and AoT legs).
- In the Developer Guide, the "Triggering a pipeline run" and "Migration
status" sections are replaced by "Pull requests from outside
contributors" and "Azure Pipelines on other branches".
- The new section names `onefuzz.yml` as the one remaining Azure
definition.
- It keeps the warning that the `Release` ruleset's Azure required check
must be lifted before cutting a new `release/2.<minor>` branch.
- `master` is merged in, including the #4555 docs rewrite. Its new
`docs/RedundancySampleTests.md` needed the Azure references removed
again. `docs/README.md` already had none.

## Reviewer notes

- **Azure DevOps side (admin action, not in this PR):** the definitions
are configured in the Azure DevOps portal. With the YAML gone, `master`
pushes give the CI and preview definitions nothing to run. Disabling
definition 14's push trigger/schedule, definition 16's build-completion
trigger, and the stale definition 13 stops any residual "file not found"
runs. Leave the OneFuzz definition enabled.
- `master378`, the 1.x `release/*` lines and the frozen `release/2.0.0`
keep their own `azure-pipelines.yml`. This PR only touches `master`.

## Related Issues

- Follow-up to #4513 (GitHub Actions for CI and nightly).

## 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.

This PR removes a test and adds none. Locally, at `9b00da817`, the full
`Opc.Ua.Tools.Tests` suite passed on net10.0: 884 passed, 1 skipped. The
later commits (OneFuzz restore, merge with `master`) touch only YAML and
docs.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 07:44:36 +02:00
Copilot df927cbb85 Honor cancellation during TCP endpoint connection (#4553)
# Description

`SelectEndpointAsync` accepted a cancellation token, but legacy TCP
connection attempts could wait for socket timeout. Pending connection
cancellation now consistently surfaces as `OperationCanceledException`.

- **TCP cancellation**
  - Preserve native cancellation-token socket connect on modern targets.
- Normalize legacy disposal-triggered `SocketException` /
`ObjectDisposedException` to cancellation.

```csharp
catch (SocketException) when (ct.IsCancellationRequested)
{
    throw new OperationCanceledException("Connection attempt was cancelled.", ct);
}
```

- **Coverage**
- Add transport coverage for a pre-cancelled connection attempt and
verify no endpoint is established.

## Related Issues

## 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.
- [ ] 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.

<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes #4551

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>
Co-authored-by: Marc Schier <marcschier@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
2026-09-28 09:37:36 +00:00
romanett e0c56a7c00 Replay the fuzz crash corpus from the fuzz-corpus orphan branch (#4539)
# Description

Since the move to GitHub Actions, CI replays only the checked-in fuzz
corpus. The full crash corpus used to be the Azure DevOps secure file
`FuzzingArtifacts.zip`, which `.azurepipelines/test.yml` overlaid onto
`Opc.Ua.Encoders.Fuzz.Tests`. The nightly `private-corpus` job that was
meant to replace it depended on an Azure storage account, federated
credentials and a protected environment. None of these were ever
provisioned, so the job never ran.

**The corpus now lives on the orphan branch
[`fuzz-corpus`](https://github.com/OPCFoundation/UA-.NETStandard/tree/fuzz-corpus)**
(commit `843ba944a7b144a7ae6429151846d9e5ab28d29f`, already pushed):
- It contains 22,267 `crash-*`, 25 `slow-unit-*` and 1 `timeout-*` input
under `Opc.Ua.Encoders.Fuzz.Tests/Assets/`.
- The files are byte-identical to the secure file (SHA-256
`85198e46…fe1d61b`), recovered from the `drop` artifact of Azure build
18397.
- The branch shares no history with `master`, so clones of `master`
don't get 22k extra files in their checkout.

**Why the corpus is no longer treated as private:**
- The inputs date from April–June 2024.
- The `drop` artifacts could be downloaded anonymously.
- Every master build printed up to about 7k base64 reproducers into
anonymously readable Azure logs.
- Current `master` replays every input without a test failure.

New, unfixed crashes still go through
[SECURITY.md](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/SECURITY.md)
and are added to the branch only after the fix ships.

## Changes

- **`nightly.yml`:** `private-corpus` is replaced by `crash-corpus`. The
new job:
- checks out the commit pinned in `FUZZ_CORPUS_COMMIT` (no secrets,
environment or OIDC);
- verifies the SHA and copies each top-level folder over
`fuzzing/<folder>`;
- runs `Opc.Ua.Encoders.Fuzz.Tests` on the 12 non-macOS profiles through
the shared `run-dotnet-tests` action and uploads
`crash-corpus-results-*`.

The `run_private_corpus` input and the INCOMPLETE label are removed. The
job runs on every scheduled and manual run, and the nightly summary
fails if it was skipped.
- **`fuzzing/CrashCorpus.md` (new):** the Azure history, branch layout,
how CI replays the corpus, local replay, and how to add inputs and move
the pin. `Fuzzing.md` links to it.
- **`docs/DeveloperGuide.md`:** the private-corpus section and its
secrets table are replaced, and the migration to-do list is updated.
- **`nightly.yml` AOT job (fix for a failure found by the first nightly
run):** the Linux and macOS legs looked up the published binary by
`BaseName`/`Extension`. A NativeAOT executable has no extension on those
platforms, so PowerShell splits `Opc.Ua.Aot.Tests` into `Opc.Ua.Aot` +
`.Tests`, and every leg reported *No NativeAOT executable was produced*
even though publish succeeded ([run
36017168717](https://github.com/OPCFoundation/UA-.NETStandard/actions/runs/36017168717),
`aot-ubuntu-latest`). The lookup now matches the exact file name. A
failure now reports the publish errors, a missing publish directory, or
which files were produced instead.
- **`Opc.Ua.Redundancy.Samples.Tests` / `Opc.Ua.WotCon.Samples.Tests`
(fix for failures found by the first nightly run):** the test runner's
framework guard rejected both projects on the nightly net8.0, net9.0,
net472 and netstandard2.1 profiles. Redundancy built a net48 placeholder
for every target except net10.0, and WotCon was pinned to net10.0. Both
now build the requested target on net8.0/9.0/10.0 and use
`RestrictForLegacyTfm` to become explicit not-applicable shells on
net48, net472, netstandard2.0 and netstandard2.1. The redundancy tests
no longer hard-code `bin/<cfg>/net10.0` when locating the sample
binaries, and `SampleAppProcess` falls back to an `object` lock below
net9.0. The net48 placeholder remains for Azure's plain `dotnet test -f
net48` leg. Checked locally by building Redundancy for
`CustomTestTarget=net9.0` and WotCon for `netstandard2.1`.
- **`DefaultServerRedundancyHandler` (fix for a failure found by the
first nightly run):** `ResolveServerAsync` gave the resolver a token
linked to the caller's, but waited on the caller's token directly. On
cancellation the wait could finish, and its `using` could dispose the
linked source, before the linked source's own callback had run. That
unhooks it, so the in-flight peer lookup never saw the cancellation
(`PeerDiscoveryPropagatesCallerCancellationAsync`, netstandard2.0 build
on net48). The wait now uses the linked token. A timeout is still logged
as a discovery failure, as before. A companion test fix makes
`PeerTimeoutPreservesOtherEndpointsAndCoalescesPendingDiscoveryAsync(False)`
deterministic. The timed-out lookup drains on the thread pool, so the
test's retry could join it and then have its own time limit expire
before it started a fresh lookup. The test now waits for the second
resolver call before advancing the fake clock.
- **`Session` keep-alive logging (found while investigating the net48
nightly failures):** `OnKeepAliveError` and the keep-alive read failure
log used `Endpoint?.EndpointUrl`. `ClientBase.Endpoint` dereferences the
transport channel, so once the channel was detached it threw a
`NullReferenceException` on every keep-alive tick, and
`BadNoCommunication` never reached the `KeepAlive` subscribers. Both now
log the configured endpoint's URL. Three new `SessionKeepAliveTests`
cover a detached channel.
- **`ClientTestFramework` shared session (fix for 67
`Opc.Ua.Sessions.Tests` failures on the net48 nightly):** the
fixture-wide shared session used the 10 s default session timeout. A
test-host stall of about 11–18 s while other fixtures restarted their
servers let the server expire it, and every later test in the
`ClientTest("https")` and `ClientTest("opc.https")` fixtures failed with
`BadSessionIdInvalid`. The shared session now requests
`SharedSessionTimeout` = 120 s; per-test sessions keep 10 s.
- **`run-dotnet-tests.ps1`:** `-QuietOutput` help text only; it no
longer names the removed job.

The Azure secure file stays in place, because `master378` and the 1.x
release pipelines keep their own `test.yml` and still use it.

## Verification

- `nightly.yml` parses as valid YAML, and no job, output or summary
field still refers to the removed job.
- I ran the job's overlay script against a clone of `fuzz-corpus` at the
pinned commit. It overlaid all 22,293 files, left the tracked
`Assets/Repo` untouched, removed the checkout folder and wrote the
summary line.
- **Not yet run:** the workflow itself. It is
`schedule`/`workflow_dispatch` only, so it needs a manual `nightly.yml`
run on this branch. On master the corpus has so far only been replayed
on Windows (net48/net10.0 in Azure), so the Linux legs are new ground.

## Follow-ups (admin)

- Add a ruleset for `fuzz-corpus`: require pull requests and block force
pushes and deletion, so the pinned commit stays reachable.
- The `fuzz-private-corpus` environment, if it was ever created, can be
deleted.

## Related Issues

- No issue exists yet; this is part of the Azure DevOps → GitHub Actions
migration (#4513, #4524).

## 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-09-25 07:49:00 +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