Skip to content

Fix Opc.Ua.Client audit findings (security fallback, reconnect, subscriptions, redundancy, browse, files, WebApi) - #4572

Merged
marcschier merged 91 commits into
masterfrom
romanett/opc-ua-client-bugs-786838
Sep 29, 2026
Merged

marcschier merged 91 commits into
masterfrom
romanett/opc-ua-client-bugs-786838

Conversation

@romanett

@romanett romanett commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Fixes the findings of a bug audit of Opc.Ua.Client and Opc.Ua.Client.ComplexTypes, done on master plus #4516 (now merged).

  • Audit: 82 findings across 12 areas, each checked against the OPC UA specification. Final severities: 2 High, 37 Medium, 43 Low.
  • Fixes: 76 fixed, 4 partial, 2 skipped (see Not fixed).
  • Review: a follow-up review of the fixes found 15 more defects in them, all fixed here.
  • Tests: every fix has a regression test, except a few races that cannot be reproduced deterministically; the commit messages say which.

The finding IDs (L<area>-<n> for the audit, G<n> for the review) appear in the commit titles.

Behaviour and user-facing changes

Security and endpoint selection

  • Reconnect no longer drops to weaker security (L3-1, High). When the server rejects the configured security policy or mode during reconnect, SessionReconnectHandler no longer switches to whatever endpoint the server advertises, None included. It only picks an endpoint at least as strong as the configured one, judged by a locally computed security level. The server-advertised SecurityLevel is not trusted. If nothing qualifies, reconnect fails with the original error (new log event SessionReconnectHandler + 13) and keeps retrying with the original settings.
  • CoreClientUtils.SelectEndpoint(useSecurity: true) never returns a SecurityMode None endpoint over opc.tcp (L9-9, High). It now returns null when no secure endpoint matches.
    • When the discovery URL is https/opc.https, an HTTPS endpoint with SecurityMode None is still accepted as a last resort, because TLS protects it (Part 6 §7.4.1) (G17).
    • Endpoints with SecurityMode Invalid no longer count as secure.
    • GlobalDiscoveryServerClient and ServerPushConfigurationClient now throw BadSecurityPolicyRejected when no secure endpoint matches, instead of passing null on. The ConsoleReferenceClient sample reports this with a clear error.
  • No Bearer/Basic credentials over http:// (L12-2). WebApiClient.Create and WebApi channel open/reconnect reject them with BadSecurityChecksFailed. The public WebApiClient(HttpClient, …) constructor also rejects them when the client's BaseAddress is not https, and every send checks the address it actually goes to (covers a BaseAddress set later).

Session, keep-alive and identity

  • Faster detection of a silent server (L1-1). The keep-alive Read is now bounded by its TimeoutHint (2 × KeepAliveInterval), which is what Part 4 §7.32 says the client should do. It then fails with BadNoCommunication, so reconnect starts after about 2 × KeepAliveInterval instead of OperationTimeout (120 s by default).
  • Operations fail fast during a channel-manager recovery (L1-2, G9). UpdateIdentityAsync, TransferSubscriptionsAsync and ReactivateSubscriptionsAsync fail with BadSecureChannelClosed (retryable) instead of deadlocking. This covers the operation holding the reconnect lock and any queued behind it.
  • Closing or disposing from a handler no longer waits on itself (L1-3, G5, G6).
    • Session and ManagedSession Close/Dispose from a Notification handler no longer wait 30 s for their own background drain.
    • Disposing a ManagedSession from a keep-alive PublishStatusChanged handler no longer deadlocks.
    • Tasks spawned by a handler no longer count as "the handler" (the owner marker is scoped to the work item).
  • Switching to an ECC user-token policy takes two ActivateSession calls (L1-4, G4). This follows Part 6 §6.8.2 (fetch the ephemeral key, then activate).
    • If the server returns no key, the switch fails with BadSecurityPolicyRejected.
    • If the second activation is rejected, the old identity stays on the override policy with the key just fetched. Restoring the old key was tried: against the reference server the next reconnect then failed to decrypt the identity token.
  • ManagedSession.ConfiguredEndpoint follows failover (L2-6). It now reports the inner session's endpoint, so it changes after a failover or network-path rotation.
  • An explicit identity update replaces the identity provider (L2-7). ManagedSession.UpdateSessionAsync(identity) stops the provider's refresh loop and drops the provider for good. If the update fails, the loop is restarted.
  • Reconnect waits for in-flight calls on a healthy channel (L2-3, G8a). Reconnect/failover on a healthy channel (certificate or trust-list change, manual reconnect) waits for in-flight service calls. The wait is bounded (max(KeepAliveInterval, 1 s)) only once the channel is unhealthy (keep-alive stopped, channel not Ready), so a dead channel no longer holds recovery for OperationTimeout.
  • Smaller fixes:
    • Restore reads the server certificate as a chain; this failed on macOS (L1-5).
    • Session recovery can no longer run twice at once (L1-6).
    • Identity expiries more than 49.7 days out no longer throw (L2-2).
    • Keep-alive reconnect suppression is recalculated on a lease swap (L2-4).

Reconnect state machine and reverse connect

  • ⚠ MaxTotalReconnectTime = 0 now means "no time limit" (L3-7). Reviewer decision needed. Before, 0 disabled reconnect entirely. Negative values other than Timeout.InfiniteTimeSpan now throw ArgumentOutOfRangeException when the state machine is constructed; before, they crashed the worker. The XML docs of ReconnectPolicy/ReconnectPolicyOptions are updated. If 0 should keep meaning "no reconnect", this is a one-line revert.
  • A close racing the end of the reconnect budget is no longer lost (L3-4). It goes through the normal close instead of switching to failover.
  • The "reconnect worker" marker covers one step only (L2-1). It lasts for a single connect/reconnect/failover/close step. Keep-alive, publish and identity loops started in a step no longer count as the worker afterwards.
  • Reverse-connect strategies Undefined and Any alone are removed after the first callback (L8-1). This matches the documentation; before, they behaved like Always.
  • AddEndpoint for a URL that is already added disposes the replaced listener (L8-2).

Service calls

  • Batched calls clean up after a failed later batch (L3-2). When a later batch of Browse/BrowseNext/HistoryRead/CreateMonitoredItems fails, the continuation points and monitored items created by earlier batches are released or deleted before the exception is rethrown. This adds one request. AddNodes/RegisterNodes are not undone.
  • Batched diagnostic infos point at the right strings (L3-3). The string-table indexes are rebased when diagnostics are padded.
  • ReadBytesAsync rejects oversized responses (L3-5). It throws BadUnknownResponse when the server returns more bytes than requested, and BadEncodingLimitsExceeded beyond int range.
  • ReadByteStringInChunksAsync handles an unknown server limit (L3-6). When the server's MaxByteStringLength is 0 (absent) or larger than int, it falls back to the client's MessageContext.MaxByteStringLength. It throws only when both are unknown.

Browse and NodeCache

  • Empty browse pages with a continuation point are followed (L9-1, L9-4, L9-5, G16). Part 4 §5.9.2.1 allows a server to return an empty page with a continuation point, so browsing keeps following it. After 10 consecutive empty rounds it gives up with BadNoData and releases the continuation point.
    • BrowseAsync(NodeId) now throws instead of silently returning a truncated list.
    • BrowseStreamAsync yields a BadNoData result for that node.
    • The same cap, through one shared helper, now also protects RoleManagementClient.ListRolesAsync and the FileSystem enumerate/empty-check loops.
  • Continuation points are released on failure (L9-3). BrowseAsync(NodeId) releases them on any exception, and BrowseStreamAsync releases all points of the current page when the consumer stops early.
  • NodeCache lookups propagate cancellation (L9-6, L9-8). FindAsync, IsKnownAsync, FetchNodeAsync, FindReferenceTypeNameAsync, the reference-table fill and the type-hierarchy search now let OperationCanceledException through. Before, a cancelled lookup looked like "node does not exist".
  • A NodeClass mismatch fails only that node (L9-7). It returns BadNodeClassInvalid for that node instead of throwing KeyNotFoundException for the whole batch.
  • Smaller fixes:
    • MaxNodesPerBrowse ≥ 2^31 no longer breaks the managed browse (L9-2).
    • Null discovery URLs are skipped (L9-10).
    • ModelChangeTracker no longer cancels a joined start (L9-11).
  • Log changes: BrowserContinuationPointExistsButBrowse (Browser + 0) was removed, and BrowserServerReturnedEmptyReferencesBut now includes the round count.

Subscriptions (default engine)

  • Recovery after a transfer delivers each message exactly once (L5-1, G22). Messages already dispatched are acknowledged instead of republished and delivered again. Messages whose earlier republish failed are now republished and delivered, with the Republish flag and after newer messages, instead of being acknowledged and lost.
  • A keep-alive that proves a gap triggers republish at once (L5-5, G23). The dedup mark moves only past messages that were actually recovered, so a failed republish is retried by the next data message.
  • Item changes that fail are retried with backoff (L5-3). They are retried on the next apply pass after the state manager's 250 ms–5 s backoff, instead of six times back to back. The six attempts now span about 13 s.
  • Extreme revised intervals no longer throw (L5-4, L6-1). Very large or NaN intervals and counts revised by the server are capped (NaN falls back to the requested value). Before, the overflow could stop every publish worker.
  • SetTriggeringAsync from a callback no longer hangs (L5-7, G21). While the subscription is deleted, recreated or disposed, it returns BadOperationAbandoned. A cancelled reset no longer makes later calls fail.
  • Publish responses for unknown subscription ids (L6-3, L6-4, G20). The client no longer re-sends acknowledgements the server already processed. An unknown id seen while a creation is pending is left alone instead of being deleted.
  • Smaller fixes:
    • A null and an empty IndexRange are treated as equal, and restored snapshots use null (L5-6).
    • A failed partition attach during restore is rolled back (L6-2).
    • StreamingSubscription throws ObjectDisposedException after dispose (L6-6).

Subscriptions (classic Subscription)

  • Republish no longer abandons other missing messages (L7-1). A Republish response no longer overwrites AvailableSequenceNumbers, so other missing messages are not given up while the server still holds them.
  • Late copies are no longer delivered twice (L7-2). Copies of already-processed sequence numbers are ignored, and creating a new server subscription resets the sequence state.
  • Disposing from a keep-alive handler no longer deadlocks (L7-3). Dispose() from a keep-alive PublishStatusChanged handler can return while that callback is still finishing.
  • Transfer of a restored subscription (L7-4, G27).
    • It succeeds even when some saved items were never created.
    • The server subscription is deleted only when this session's own TransferSubscriptions just moved it and adoption failed definitively (item count mismatch or a non-transient status). Transient errors and ReactivateSubscriptionsAsync never delete it.
    • The public Subscription.TransferAsync never deletes.
  • Smaller fixes:
    • Cloned items keep their triggering links (L7-5).
    • CreateAsync clears stale server ids (L7-6).
    • A Good_SubscriptionTransferred for the client's own transfer is ignored (L7-7).
    • An item removed while its create is in flight is deleted afterwards (L7-8).
    • The publish timeout for very long keep-alives no longer overflows (L7-9).
    • A negative MaxMessageCount is handled (L7-10).
    • LastMessage tolerates a wrong notification type (L7-11).
    • Pending acks exactly 2^31 away no longer throw (L4-1).
  • Log changes: new log events Subscription + 68..70.

Redundancy

  • Warm/Hot demotion (L4-2, G1, G3).
    • Without token reuse, the demoted leader closes its session and reconnects as a fresh standby.
    • With token reuse, it abandons the shared server session locally, without CloseSession, so the new leader can reactivate it and keep its subscriptions. This uses the new internal methods ManagedSession.AbandonServerSessionAsync and Session.AbandonServerSession.
    • RoleChanged(false) is now raised before the standby connect and no longer depends on it, so CurrentSession can briefly be null.
    • Exceptions from RoleChanged handlers are logged and do not stop the teardown.
  • Promotion (L4-3, G2). A promoted standby that adopts the leader's session first closes its own server session. A retried promotion keeps the session it already adopted.
  • Client failover takeover (L4-4, partial). Subscriptions the backup prepared in advance (not yet created, TransferId set to the active client's id) are moved through the session's own transfer and keep publishing. docs/HighAvailability.md is updated.
  • ShouldFailover through the interface (L4-6). Calling it through IServerRedundancyHandler no longer fails over away from a server that is still Healthy.

Fluent / dependency injection

  • Shared DI session (L10-5). The shared session from AddClient is reconnected after a caller disposes it, and is disposed with the container. After that the connect delegate throws ObjectDisposedException.
  • New public API: ManagedSessionBuilder.WithServerRedundancyOptions(ServerRedundancyOptions) (L10-6). This is additive. The Session.ServerRedundancy options are now actually applied.
  • AddDiscoveryAndConnect rewrites the advertised address (L10-7). It replaces the host and port the server advertises with those of the discovery URL (Part 4 §5.6.2.1, NAT/containers), and prefers endpoints with the same scheme.

State machines and role management

  • GetSubStateMachineAsync returns the instance's own sub-state machine (L10-1). Before, it returned the type's placeholder.
  • WaitForStateAsync reports failures with OPC UA status codes (L10-2). It throws BadNotFound when CurrentState/Id cannot be resolved, and BadInvalidState when observation ends without reaching the state. Before, both surfaced as OperationCanceledException.
  • RoleManagementClient.ListRolesAsync pages correctly (L10-3). It follows continuation points, releases them on any failure, and throws on a Bad browse status instead of returning an empty list.
  • Role properties are read into the right fields (L10-4). Properties whose namespace cannot be resolved are skipped instead of shifting the other values.

File system, temporary files and alias names

  • Path escaping (L11-8, G12).
    • Path segments escape : and / with &, and & itself as && before those characters or at the end of a name.
    • Format and Parse round-trip all names. Typed input ending in &/ now reads the slash as part of the name.
    • A segment without a namespace prefix also matches a namespace-0 child when nothing matches in the parent's namespace.
  • Stale path cache (L11-1). When a cached lookup fails, GetInfoAsync retries once from the server. ExistsAsync and GetFileAsync report "not found" instead of throwing.
  • GenerateFileForReadAsync waits for the file (L11-2). It waits until the file-generation state machine reaches ReadTransfer (Idle counts as not started yet), and throws IOException if it reports Error. The wait is bounded only by the caller's token.
  • Non-recursive delete refuses hidden children (L11-3). It refuses when the directory holds children that the subtype options hide from enumeration. Before, the server deleted them all.
  • Alias names (L11-5, L11-6, L11-7).
    • BadMethodInvalid maps to NotSupportedException for all alias calls, so UseVerbose falls back to FindAlias.
    • A duplicate alias name resolves to all its targets.
    • If the refresh strategy fails to start, the resolver logs a warning and keeps working without automatic invalidation. New log event block AliasNameResolver = 560.
  • Null profile entries (L11-4). Null ServerProfileArray entries no longer crash GetConformanceInfoAsync.

WebApi transport

  • Failures come back as ServiceResultException (L12-1).
    • A timeout becomes BadRequestTimeout.
    • HTTP 429/503 becomes BadServerTooBusy with the Retry-After hint, on every target framework. WebApiClient checks the status itself (reusing HttpsTransportChannel.CreateServerTooBusyException), so callers of WebApiClient also get a ServiceResultException instead of an HttpRequestException for these two statuses (G15).
    • An IO error becomes BadNotConnected.
  • Timeouts (L12-1, G14).
    • OperationTimeout applies to each request. When RequestTimeout is unset, HttpClient's 100 s default no longer cuts off long Publish requests.
    • The GetEndpoints call at open is bounded by OperationTimeout, and cancelling the open now throws.
  • Core change: HttpsTransportChannel.MapRequestFailure went from private to internal so the WebApi channel can reuse it.

ComplexTypes

  • Out-of-range union selector (L12-3). A union whose SwitchField is larger than its field count now fails with BadDecodingError when decoded and BadEncodingError when encoded (Part 6 §5.2.8).
  • Verbose JSON unions (L12-4). They now decode their selected field instead of null.

Not fixed / partial

  • L2-5 (skipped): an identity provider combined with a caller-supplied reverse connection still can't connect to an undiscovered endpoint. Fixing it means choosing the identity after CreateSession, a design change.
  • L4-5 (skipped): the one-time KeyCredential token needs a fresh token on each reactivation. Today reactivation fails and the client falls back to a new session.
  • L4-4 (partial): only subscriptions the backup prepared in advance survive a takeover.
  • L3-2 (partial): AddNodes/RegisterNodes batches are not rolled back.
  • L2-3 (partial): service calls still hold the session lock for their whole round trip.
  • Known from the review, not fixed here:
    • G7: the batch cleanup request can't be cancelled.
    • G11: the DI session's close at host shutdown never reaches the server, and a closed session keeps being handed out.
    • G13: a file renamed by another client is still found under its old path.
    • G25: a clone-based transfer doesn't mark the original subscriptions.
    • G27b: the transfer item check matches by count, not by ID.
  • Outside this PR: the reference server ignores ECDHPolicyUri on ActivateSession (SessionSecurityPolicyHelper), which breaks Part 6 §6.8.2. As a result, the L1-4 two-step ECC switch ends in BadSecurityPolicyRejected against our own server.

Testing

All suites on net10.0, on the branch before the master merge. Every suite had 0 failures:

Suite Passed Skipped
Opc.Ua.Client.Tests 2698 0
Opc.Ua.Redundancy.Client.Tests 134 0
Opc.Ua.Subscriptions.Classic.Tests 57 0
Opc.Ua.Bindings.Https.WebApi.Tests 385 0
Opc.Ua.Sessions.Tests 782 379
Opc.Ua.Subscriptions.Tests 640 6
Opc.Ua.Client.ComplexTypes.Tests 2858 0

After merging current master (net10.0), every suite had 0 failures except one, a flaky new test fixed in 9f52952:

Suite Passed Skipped Failed
Opc.Ua.Client.Tests 2719 0 1 (the flaky test)
Opc.Ua.Redundancy.Client.Tests 134 0 0
Opc.Ua.Client.ComplexTypes.Tests 2858 0 0
Opc.Ua.Subscriptions.Classic.Tests 57 0 0
Opc.Ua.Bindings.Https.WebApi.Tests 397 0 0
Opc.Ua.Sessions.Tests 790 379 0
Opc.Ua.Subscriptions.Tests 640 6 0

The touched test projects also build on net48 with 0 warnings.

🤖 Generated with Claude Code

romanett and others added 30 commits September 24, 2026 22:46
Squashed from #4516 head f425376
merged onto master 57c0a32.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oints

An empty page with a continuation point is legal (Part 4 5.9.2.1): all three
browse paths now keep following it, give up after 10 empty rounds with
BadNoData and release the point. BrowseStreamAsync releases every point of
the current page on early exit and documents result correlation; the single
node browse releases on any failure; MaxNodesPerBrowse > int.MaxValue no
longer produces a negative batch size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…requested

SelectEndpoint(useSecurity: true) returned the first endpoint of any mode when
nothing matched, letting a rogue discovery answer downgrade to None; it now
returns null and the GDS clients fail with BadSecurityPolicyRejected.
DiscoverServersAsync skips null DiscoveryUrls instead of throwing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tion

An empty DataValue collided with the missing-slot sentinel and mispaired
values with NodeIds. The node cache lookups no longer turn cancellation into
"node does not exist".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…on before token reuse

A Warm/Hot ex-leader kept its leader session (subscriptions + shared token) and fought the
new leader over the mirrored session; it is now disposed and replaced by a fresh standby.
Token-reuse promotion closes the standby's own server session instead of orphaning it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A NodeClass hint that differs from the node's real class threw
KeyNotFoundException out of the whole batch; an undefined NodeClass value
failed every node. Both now yield a per-node error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A cancelled first StartTrackingAsync stopped the pump another caller had
joined. The pump is now stopped only when the last waiter abandons the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A pending ack exactly 2^31 away from the latest sequence number threw an
OverflowException inside the ack lock, dropping the publish response and
keeping the bad ack. Compare the wrap-aware distance without Math.Abs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… notification

SaveValueInCache keeps the last notification whatever its kind, so a data
change for an event item (or the reverse) made the hard cast throw
InvalidCastException into application code. Use as-casts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The copy constructor dropped TriggeredItems, so a session recreated from a
template without a successful transfer silently lost every SetTriggering
link. Links reference client handles, which clones with copyClientHandle
keep, so copy them (the server id of the triggering item is reset).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Three keep-alive intervals above int.MaxValue wrapped negative, so every
Publish was sent with a 1 s timeout; an interval capped at int.MaxValue
made PublishingStopped always true. Compute in long/double instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…livery

The trim loop called RemoveFirst on an empty cache when MaxMessageCount
was negative, so every worker pass threw before any notification was
delivered and unprocessed messages piled up. Stop trimming when empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onger deadlocks

PublishStatusChanged(Stopped) is raised on the publish timer callback, and
Dispose waited for the timer's running callbacks, i.e. for itself. Mark the
timer callback as a dispatch context so Dispose takes the non-blocking path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…resh

The discovery reply is unauthenticated; retrying with Invalid/null policy let a
misconfigured server or attacker downgrade the session to SecurityMode None.
Fall back only to an endpoint with equal or stronger locally computed security.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A chunk longer than requested corrupted the value and looped until OOM or int
overflow; reject it and stop before the next range overflows int.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… provided

An absent (0) or > int.MaxValue server MaxByteStringLength made
ReadByteStringInChunksAsync throw instead of using MessageContext.MaxByteStringLength.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idation, worker flow

L3-4: TransitionToFailover no longer overwrites a concurrent Closing state.
L3-7: MaxTotalReconnectTime 0 means unlimited, other negatives fail at construction.
L2-1: worker flow is scoped per state step so spawned background work stops passing as the worker.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Restore/Load keep the ServerId of the previous session, so when the
subscription was created instead of transferred CreateAsync took those items
as created and silently skipped them. Reset the server ids on create.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RemoveItem only queued created items, so an item removed while its
CreateMonitoredItems call was in flight was created on the server and never
deleted. After the create results are applied, queue such items for deletion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tics string table

L3-2: when a later batch fails, release browse/history continuation points and delete
monitored items created by completed batches (Part 4 7.9) before rethrowing.
L3-3: rebase merged DiagnosticInfo string indexes even when missing diagnostics are padded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…l hosts

L8-1: Undefined (and bare Any) registrations default to Once as documented instead of Always.
L8-2: re-adding an endpoint disposes the replaced manual host instead of leaking it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…elete unadoptable ones

The restore-from-storage transfer compared GetMonitoredItems against every
saved item, so one item that was never created failed the transfer, and the
server subscription just moved to the session stayed alive as an orphan.
Count only created items, and delete the server subscription if it cannot
be adopted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… numbers

The republish path passed an empty AvailableSequenceNumbers list, which the
subscription stored, so other missing messages were given up as
BadMessageNotAvailable and pending acks were pruned while the server still
held them. Republished messages now keep the last Publish response's list
and all pending acks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gain

A Publish response racing its Republish (or a message arriving after its
placeholder expired) was appended at the list tail and delivered a second
time, and a copy of a processed entry was re-marked unprocessed. Ignore
copies that are not newer than the last processed sequence number, never
reprocess a processed entry, and reset the sequence state when a new server
subscription is created.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…client made

The unsolicited-transfer check looked at the old session's Reconnecting flag,
which an explicit live-to-live transfer never sets, so the notification sent
to the old session stopped the publish worker (ReportOnly) or recreated the
subscription on the new session. The session now marks subscriptions while
it transfers them, and notifications received during or before such a
transfer are ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After a reconnect that transfers the subscription onto the same object, unacknowledged but already dispatched messages are still listed as available (Part 4 5.14.7.2). Acknowledge them instead of republishing and dispatching them a second time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A keep-alive carries the next sequence number (Part 4 5.14.1.1), so one more than one ahead of the last data message proves the messages in between were lost. Republish them right away instead of waiting for the next data message, which on quiet subscriptions may never come.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sync

Client handles need not be unique on the server (Part 4 7.21); a lost create response that is retried leaves two server items with the same handle. Build the handle map before clearing the item table, keep the bound item and delete the extra server items instead of throwing and losing every item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…pass

A transient per-item failure was retried immediately until the 6-attempt budget was exhausted within milliseconds, leaving the item permanently un-created. Stop the pass once only already-failed changes remain so the state manager backoff spaces the retries (also stops a hot loop on a delete that keeps failing).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A conforming server can revise publishing interval and keep-alive count so their product exceeds TimeSpan.MaxValue (Part 4 5.14.2.2); a NaN interval is non-conforming but possible. The throwing TimeSpan APIs aborted CreateSubscription/CreateMonitoredItems after the server succeeded and faulted every publish worker in a respawn loop. Use saturating conversions (L6-1 regression test lands with the L6-2 commit, same test hunk).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
romanett and others added 5 commits September 25, 2026 10:43
Master now contains PR #4516 (46fa7d3) at a later head than the
squashed copy this branch was based on (46fc78c). The branch's
client fixes (46fc78c..cb0764a) were replayed onto master with
46fc78c as the merge base, so only the two real overlaps needed
resolving:
- ManagedSession: master inlined the same lease-swap reseed as the
  L2-4 fix; kept the SeedChannelReconnectTracking helper.
- FakeManagedSubscription: kept the OnIsCreationInProgress hook and
  master's new IsIntentionallyDeleted property.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Background work items can run concurrently, so the test could read
the disposal task before the spawner item assigned it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@marcschier
marcschier marked this pull request as ready for review September 28, 2026 18:06
Copilot AI lite review requested due to automatic review settings September 28, 2026 18:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🟡 Changes recommended

Unresolved security, race-condition, file-readiness, continuation-point, and Web API timeout findings block approval.

Review effort: Lite
Findings: 2 High severity · 1 Medium severity

Open (3)
What changed in this PR

This PR addresses a broad audit of OPC UA client security, reconnect/recovery, subscriptions, browsing, redundancy, file transfer, Web API transport, and ComplexTypes.

Changes:

  • Hardened security endpoint selection, credential handling, reconnects, and transport failures.
  • Fixed subscription, session, browse, redundancy, file-system, and state-machine edge cases.
  • Added extensive regression tests and documentation.
File Summary
tests/​Opc.Ua.Client.Tests/​Utils/​AsyncReaderWriterLockTests.cs Tests bounded writer draining.
tests/​Opc.Ua.Client.Tests/​Subscription/​MonitoredItemTests.cs Tests index-range normalization.
tests/​Opc.Ua.Client.Tests/​Subscription/​MonitoredItemManagerTests.cs Tests deferred retries.
tests/​Opc.Ua.Client.Tests/​Subscription/​Fakes/​FakeMessageAckQueue.cs Adds session-availability test support.
tests/​Opc.Ua.Client.Tests/​Subscription/​Fakes/​FakeManagedSubscription.cs Adds creation-state test support.
tests/​Opc.Ua.Client.Tests/​Subscription/​Classic/​ClassicMonitoredItemCoverageTests.cs Tests triggering-link cloning.
tests/​Opc.Ua.Client.Tests/​Streaming/​StreamingSubscriptionTests.cs Tests disposal races.
tests/​Opc.Ua.Client.Tests/​StateMachines/​StateMachineTypeClientExtensionsTests.cs Tests state observation failures.
tests/​Opc.Ua.Client.Tests/​StateMachines/​FiniteStateMachineTypeClientExtensionsTests.cs Tests instance sub-state resolution.
tests/​Opc.Ua.Client.Tests/​Session/​UpdateIdentityAsyncTests.cs Tests ECC identity updates.
tests/​Opc.Ua.Client.Tests/​Session/​SessionReconnectHandlerTests.cs Tests secure fallback selection.
tests/​Opc.Ua.Client.Tests/​Session/​SessionExtensionsTests.cs Tests byte-string limit fallback.
tests/​Opc.Ua.Client.Tests/​Session/​SessionClientExtensionsTests.cs Tests oversized byte responses.
tests/​Opc.Ua.Client.Tests/​Session/​SessionClientBatchTests.cs Tests batch cleanup and diagnostics.
tests/​Opc.Ua.Client.Tests/​Session/​ServerRedundancyHandlerTests.cs Tests healthy-server failover decisions.
tests/​Opc.Ua.Client.Tests/​Session/​ProactiveRefreshTests.cs Tests identity refresh behavior.
tests/​Opc.Ua.Client.Tests/​Session/​ManagedSessionTests.cs Tests handler-triggered disposal.
tests/​Opc.Ua.Client.Tests/​Session/​ManagedSessionReconnectTests.cs Tests recovery concurrency.
tests/​Opc.Ua.Client.Tests/​Session/​ConnectionStateMachineTests.cs Tests reconnect budgets and close races.
tests/​Opc.Ua.Client.Tests/​Session/​ClientFailoverCoordinatorTests.cs Tests prepared subscription takeover.
tests/​Opc.Ua.Client.Tests/​Session/​ClassicSubscriptionEngineTests.cs Tests republish and sequence handling.
tests/​Opc.Ua.Client.Tests/​NodeCache/​NodeCacheTests.cs Tests cancellation and empty values.
tests/​Opc.Ua.Client.Tests/​NodeCache/​NodeCacheContextTests.cs Tests node-class mismatches.
tests/​Opc.Ua.Client.Tests/​ModelChange/​ModelChangeTrackerTests.cs Tests joined tracking starts.
tests/​Opc.Ua.Client.Tests/​Historian/​HistoryClientUnitTests.cs Tests null profile handling.
tests/​Opc.Ua.Client.Tests/​FileSystem/​UaPathTests.cs Tests escaped path names.
tests/​Opc.Ua.Client.Tests/​FileSystem/​TemporaryFileTransferClientTests.cs Tests file-generation state handling.
tests/​Opc.Ua.Client.Tests/​FileSystem/​FileSystemSessionHarness.cs Adds deletion simulation support.
tests/​Opc.Ua.Client.Tests/​FileSystem/​FileSystemClientPathResolutionTests.cs Tests path round trips.
tests/​Opc.Ua.Client.Tests/​FileSystem/​FileSystemClientCrudTests.cs Tests deletion and stale-cache behavior.
tests/​Opc.Ua.Client.Tests/​ClientBuilder/​ReverseConnectManagerTests.cs Tests reverse-connect strategies.
tests/​Opc.Ua.Client.Tests/​ClientBuilder/​OpcUaClientBuilderTests.cs Tests managed-session ownership.
tests/​Opc.Ua.Client.Tests/​ClientBuilder/​OpcUaClientBuilderExtensionsCoverageTests.cs Tests redundancy and discovery configuration.
tests/​Opc.Ua.Client.Tests/​AuditRegressions/​SessionCoreAuditRegressionTests.cs Adds session audit regressions.
tests/​Opc.Ua.Client.Tests/​AuditRegressions/​CoreClientUtilsAuditRegressionTests.cs Adds endpoint-selection regressions.
tests/​Opc.Ua.Client.Tests/​AliasNames/​AliasNameResolverTests.cs Tests alias refresh and merging.
tests/​Opc.Ua.Client.ComplexTypes.Tests/​Types/​EncoderTests.cs Tests union encoding and decoding.
src/​Opc.Ua.Gds.Client/​ServerPushConfigurationClient.cs Rejects missing secure endpoints.
src/​Opc.Ua.Gds.Client/​GlobalDiscoveryServerClient.cs Rejects missing secure endpoints.
src/​Opc.Ua.Core/​Stack/​Https/​HttpsTransportChannel.cs Shares HTTP failure mapping.
src/​Opc.Ua.Client/​WebApi/​WebApiTransportChannel.cs Adds Web API timeout and failure mapping.
src/​Opc.Ua.Client/​WebApi/​WebApiClient.cs Blocks credentials over plaintext HTTP.
src/​Opc.Ua.Client/​Utils/​AsyncReaderWriterLock.cs Adds bounded writer draining.
src/​Opc.Ua.Client/​Subscription/​Subscription.cs Improves resets and saturation handling.
src/​Opc.Ua.Client/​Subscription/​Streaming/​StreamingSubscription.cs Fixes disposal races.
src/​Opc.Ua.Client/​Subscription/​SaturatingTimeSpan.cs Adds safe duration conversions.
src/​Opc.Ua.Client/​Subscription/​MonitoredItemStateSnapshot.cs Normalizes empty index ranges.
src/​Opc.Ua.Client/​Subscription/​MonitoredItemManager.cs Defers retries and handles duplicate handles.
src/​Opc.Ua.Client/​Subscription/​MonitoredItem.cs Handles revised intervals and option comparisons.
src/​Opc.Ua.Client/​Subscription/​Classic/​MonitoredItem.cs Preserves triggering links.
src/​Opc.Ua.Client/​StateMachines/​StateMachineTypeClientExtensions.cs Reports state failures accurately.
src/​Opc.Ua.Client/​StateMachines/​FiniteStateMachineTypeClientExtensions.cs Resolves instance sub-state machines.
src/​Opc.Ua.Client/​Session/​Subscription/​SessionEngineContext.cs Updates background notification dispatch.
src/​Opc.Ua.Client/​Session/​Subscription/​ClassicSubscriptionEngine.cs Fixes republish and sequence handling.
src/​Opc.Ua.Client/​Session/​SessionExtensions.cs Handles unknown byte-string limits.
src/​Opc.Ua.Client/​Session/​SessionClientExtensions.cs Validates oversized byte responses.
src/​Opc.Ua.Client/​Session/​Session.ChannelManager.cs Coordinates recovery locking.
src/​Opc.Ua.Client/​Session/​Redundancy/​IServerRedundancyHandler.cs Prevents failover from healthy servers.
src/​Opc.Ua.Client/​Session/​Redundancy/​ClientFailoverCoordinator.cs Transfers prepared subscriptions.
src/​Opc.Ua.Client/​Session/​Reconnect/​SessionReconnectHandler.cs Prevents insecure reconnect fallback.
src/​Opc.Ua.Client/​Session/​Reconnect/​ReconnectPolicyOptions.cs Documents reconnect-budget semantics.
src/​Opc.Ua.Client/​Session/​Reconnect/​ReconnectPolicy.cs Documents reconnect-budget semantics.
src/​Opc.Ua.Client/​Session/​Reconnect/​ConnectionStateMachine.cs Scopes worker flow and close transitions.
src/​Opc.Ua.Client/​Roles/​RoleManagementClient.cs Adds paged role browsing.
src/​Opc.Ua.Client/​ReverseConnectManager.cs Fixes endpoint replacement and strategy defaults.
src/​Opc.Ua.Client/​NodeCache/​NodeCache.cs Propagates cancellation and preserves value slots.
src/​Opc.Ua.Client/​ModelChange/​ModelChangeTracker.cs Coordinates concurrent tracking starts.
src/​Opc.Ua.Client/​Historian/​HistoryClient.Extras.cs Skips null historical profiles.
src/​Opc.Ua.Client/​Fluent/​ManagedSessionBuilder.cs Adds redundancy options configuration.
src/​Opc.Ua.Client/​FileSystem/​UaPath.cs Escapes reserved path characters.
src/​Opc.Ua.Client/​FileSystem/​TemporaryFileTransferClient.cs Waits for generated files.
src/​Opc.Ua.Client/​EventIds.cs Adds alias resolver event IDs.
src/​Opc.Ua.Client/​CoreClientUtils.cs Hardens secure endpoint selection.
src/​Opc.Ua.Client/​AliasNames/​AliasNameResolver.cs Handles refresh failures and duplicate aliases.
src/​Opc.Ua.Client/​AliasNames/​AliasNameClientErrors.cs Maps unsupported alias methods.
src/​Opc.Ua.Client.ComplexTypes/​Types/​UnionComplexType.cs Validates union selectors and JSON fields.
samples/​Reference/​ConsoleReferenceClient/​UAClient.cs Reports endpoint-selection failures.
docs/​HighAvailability.md Documents prepared subscription takeover.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/Opc.Ua.Client/ModelChange/ModelChangeTracker.cs Outdated
Comment thread src/Opc.Ua.Client/WebApi/WebApiClient.cs
Comment thread src/Opc.Ua.Client/FileSystem/TemporaryFileTransferClient.cs Outdated
- Tests used string.Contains(string, StringComparison) and
  Task.Wait(TimeSpan, CancellationToken), which net472/net48 lack.
- WebApiClient now maps HTTP 429/503 to BadServerTooBusy (with the
  Retry-After hint) before EnsureSuccessStatusCode on every target
  framework, reusing HttpsTransportChannel.CreateServerTooBusyException.
  The channel relied on HttpRequestException.StatusCode, which only
  exists on .NET 5+, so the netstandard2.1 build reported
  BadUnknownResponse (review finding G15 / RE-1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Code coverage

✅ Coverage gate passed.

Check Result Threshold
✅ Project line rate 87.91% (281470/320191 lines) >= 70.00%
✅ Project branch rate 78.42% >= 60.00%
✅ Patch coverage 88.45% (1554/1757 changed lines) >= 75.00% (> 100 changed lines)
ℹ️ Baseline delta (advisory) +14.31 pp 73.60% recorded
Uncovered changed lines
  • src/Opc.Ua.Client/StateMachines/StateMachineTypeClientExtensions.cs: 213, 214
  • src/Opc.Ua.Client/Subscription/SubscriptionManager.cs: 1210, 1212, 1213, 1214, 1266, 1267, 1272, 1273, 1274, 1275, 2437, 2456, 2457
  • src/Opc.Ua.Client/Fluent/OpcUaClientBuilderExtensions.cs: 1686, 1695, 1699, 1710, 1711, 1712, 1713, 1714, 1715, 1716, 1717, 1718, 1719, 1720, 1721, 1722, 1723, 1724, 1725, 1733
  • src/Opc.Ua.Client/NodeCache/NodeCache.cs: 734, 762, 1133
  • src/Opc.Ua.Client/Session/ManagedSession.cs: 1028, 1598, 1599, 1601, 1603, 1604
  • src/Opc.Ua.Client/Subscription/SaturatingTimeSpan.cs: 80, 88
  • src/Opc.Ua.Client/Session/Redundancy/ClientReplicaCoordinator.cs: 199, 201, 202, 218, 220, 221, 322, 323, 324, 365, 367, 368
  • src/Opc.Ua.Client/Subscription/Classic/Subscription.cs: 1975, 1977, 1978, 1979, 1980, 1981, 2683
  • src/Opc.Ua.Client/StateMachines/FiniteStateMachineTypeClientExtensions.cs: 295, 297, 298, 300, 301, 302
  • src/Opc.Ua.Client/Session/Reconnect/SessionReconnectHandler.cs: 672, 673, 676, 677, 687, 688, 689, 690, 691, 693, 694, 697, 698, 699, 700, 701, 703, 781, 783, 784, 785, 786, 787, 788, 790, 791, 792, 793, 794
  • src/Opc.Ua.Client/WebApi/WebApiClient.cs: 350, 351, 352
  • src/Opc.Ua.Client/Session/Redundancy/IServerRedundancyHandler.cs: 151, 152, 153, 154
  • src/Opc.Ua.Client/Subscription/MessageProcessor.cs: 512, 516, 640, 789, 845, 868, 869, 870, 872
  • src/Opc.Ua.Gds.Client/ServerPushConfigurationClient.cs: 390, 391, 392, 393, 397
  • src/Opc.Ua.Client/Subscription/MonitoredItemManager.cs: 851
  • src/Opc.Ua.Client/Session/SessionExtensions.cs: 243
  • src/Opc.Ua.Client/Browser.cs: 922, 924, 925
  • src/Opc.Ua.Client/Fluent/ManagedSessionBuilder.cs: 556
  • src/Opc.Ua.Client/WebApi/WebApiTransportChannel.cs: 434, 436, 437, 438, 439, 440, 441
  • src/Opc.Ua.Client/Session/Session.cs: 1962, 1963, 2009, 2010, 2012, 2013, 2087, 2090, 2141, 2585, 2587, 5224, 5225, 5226, 5227
  • src/Opc.Ua.Client/CoreClientUtils.cs: 104, 108
  • src/Opc.Ua.Client/Subscription/Subscription.cs: 476
  • src/Opc.Ua.Client/NodeCache/NodeCacheContext.cs: 426, 430, 431, 432, 433
  • src/Opc.Ua.Client/Session/SessionClientBatched.cs: 232, 233, 552, 553, 554, 555, 556, 557, 1561, 1565, 1578, 1579, 1581, 1582, 1584, 1585, 1586, 1587, 1588, 1589, 1590, 1591, 1594, 1596, 1600, 1601, 1602, 1603, 1604, 1605, 1606, 1607, 1608, 1611, 1612, 1632
  • src/Opc.Ua.Client/FileSystem/FileSystemClient.cs: 216, 219, 1153
  • src/Opc.Ua.Client/Roles/RoleManagementClient.cs: 131, 132
  • src/Opc.Ua.Client/FileSystem/TemporaryFileTransferClient.cs: 211, 238
  • src/Opc.Ua.Client/Session/SessionClientExtensions.cs: 854, 855, 856

Coverage is above the recorded baseline - consider ratcheting coverage-thresholds.json.

Thresholds live in coverage-thresholds.json. Whole report before exclusions: line 87.01%, branch 77.58%.

… readiness

- ModelChangeTracker: decide "last start waiter" and detach the
  generation under one lock, so a caller joining between the two can
  no longer have its generation cancelled (review G18).
- WebApiClient: the public HttpClient constructor now rejects
  Bearer/Basic credentials over a non-https BaseAddress before
  installing them, and every send checks the effective base address.
- TemporaryFileTransferClient: only ReadTransfer ends the wait for an
  asynchronously generated file; Idle means not started (Part 20 4.4.3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 84.74673% with 268 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.09%. Comparing base (ea9a8a0) to head (5e7948b).

Files with missing lines Patch % Lines
src/Opc.Ua.Client/Session/SessionClientBatched.cs 76.58% 36 Missing and 1 partial ⚠️
...lient/Session/Reconnect/SessionReconnectHandler.cs 52.30% 29 Missing and 2 partials ⚠️
...c.Ua.Client/Fluent/OpcUaClientBuilderExtensions.cs 64.86% 20 Missing and 6 partials ⚠️
src/Opc.Ua.Client/Session/Session.cs 87.06% 14 Missing and 12 partials ⚠️
src/Opc.Ua.Client/Session/ManagedSession.cs 75.00% 6 Missing and 7 partials ⚠️
...ent/Session/Redundancy/ClientReplicaCoordinator.cs 79.68% 12 Missing and 1 partial ⚠️
src/Opc.Ua.Client/Subscription/MessageProcessor.cs 82.19% 8 Missing and 5 partials ⚠️
.../Opc.Ua.Client/Subscription/SubscriptionManager.cs 76.36% 13 Missing ⚠️
...Opc.Ua.Client/Subscription/Classic/Subscription.cs 90.00% 7 Missing and 5 partials ⚠️
src/Opc.Ua.Client/WebApi/WebApiTransportChannel.cs 81.25% 7 Missing and 2 partials ⚠️
... and 25 more
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##           master    #4572      +/-   ##
==========================================
+ Coverage   82.06%   82.09%   +0.03%     
==========================================
  Files        2096     2097       +1     
  Lines      318898   320191    +1293     
  Branches    55673    55902     +229     
==========================================
+ Hits       261705   262871    +1166     
- Misses      38468    38543      +75     
- Partials    18725    18777      +52     
Flag Coverage Δ
actions 82.09% <84.74%> (+0.03%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
...c.Ua.Client.ComplexTypes/Types/UnionComplexType.cs 66.66% <100.00%> (+1.44%) ⬆️
.../Opc.Ua.Client/AliasNames/AliasNameClientErrors.cs 66.66% <100.00%> (+1.44%) ⬆️
src/Opc.Ua.Client/AliasNames/AliasNameResolver.cs 85.33% <100.00%> (+1.39%) ⬆️
src/Opc.Ua.Client/FileSystem/UaPath.cs 99.09% <100.00%> (+0.47%) ⬆️
...rc/Opc.Ua.Client/Historian/HistoryClient.Extras.cs 87.55% <100.00%> (ø)
src/Opc.Ua.Client/ReverseConnectManager.cs 86.09% <100.00%> (+0.04%) ⬆️
...Opc.Ua.Client/Session/Reconnect/ReconnectPolicy.cs 94.20% <ø> (ø)
...Client/Session/Reconnect/ReconnectPolicyOptions.cs 100.00% <ø> (ø)
...rc/Opc.Ua.Client/Session/Session.ChannelManager.cs 84.01% <100.00%> (+0.22%) ⬆️
...lient/Session/Subscription/SessionEngineContext.cs 91.66% <100.00%> (-0.27%) ⬇️
... and 39 more

... and 29 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@marcschier

Copy link
Copy Markdown
Collaborator

@copilot resolve the merge conflicts in this pull request

…ent-bugs-786838

# Conflicts:
#	src/Opc.Ua.Client.ComplexTypes/Types/UnionComplexType.cs

Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com>

Copilot AI commented Sep 29, 2026

Copy link
Copy Markdown
Contributor

@copilot resolve the merge conflicts in this pull request

Resolved the conflicts by merging current master in 244741fc. The sole manual conflict in UnionComplexType.cs preserves both the audit fixes and upstream validation; all 2,864 ComplexTypes net10.0 tests pass.

@CLAassistant

CLAassistant commented Sep 29, 2026 •

Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you all sign our Contributor License Agreement before we can accept your contribution.
2 out of 3 committers have signed the CLA.

✅ marcschier
✅ romanett
❌ Copilot
You have signed the CLA already but the status is still pending? Let us recheck it.

@marcschier marcschier added the ready Ready to merge once CI Passes label Sep 29, 2026
@marcschier
marcschier enabled auto-merge (squash) September 29, 2026 09:31
@marcschier
marcschier merged commit 87c35b8 into master Sep 29, 2026
197 of 198 checks passed
@marcschier
marcschier deleted the romanett/opc-ua-client-bugs-786838 branch September 29, 2026 09:34
marcschier pushed a commit that referenced this pull request Sep 29, 2026
# Description

Makes every push to `master` and `release/*` report both required
checks, so a release branch can be cut from `master`.

Two things stopped `master` commits from getting a green `images
summary`, which the `Release` ruleset requires, including when a branch
is created:

1. **Cancellation.** `CI` (`buildandtest.yml`) and `Images CI`
(`docker-image.yml`) used one concurrency group per ref with
`cancel-in-progress: true`. Each merge to `master` cancels the
still-running checks of the commit before it. A queued run in the same
group is replaced as well ("Canceling since a higher priority waiting
request … exists"). `Images CI` takes about 100 minutes, and `master`
currently gets a merge about every 90 minutes:

   | Commit | `build-and-test summary` | `images summary` |
   | --- | --- | --- |
| `87c35b84b` (#4572) | success | failure: `pumpserver` / `refserver`
cancelled after ~100 min by the #4573 push |
| `20c2d77d5` (#4573) | success | failure: `refserver` / `mcpserver` /
`pumpserver` cancelled by the #4577 push |

The ruleset has no bypass actors, so `git push origin
20c2d77:refs/heads/release/2.0` is rejected with `Required status
check "images summary" is failing`.

2. **Push path filter.** `Images CI`'s push trigger had `paths-ignore:
docs/**, tests/**`. A documentation- or test-only push to `master` or
`release/*` never started the workflow, so it never reported `images
summary` and could never be a cut point.

## Changes

- **Concurrency (both workflows):** pushes to protected lines (`master`,
`main`, `master378`, `release/*`) get one concurrency group per commit
(`github.sha`) and never cancel, so every commit runs its checks to
completion. Pull requests and other branches are unchanged: they group
by PR number or ref and cancel superseded runs.
- The per-commit group is needed as well as `cancel-in-progress: false`.
With a shared group GitHub still replaces a *queued* run.
- **`Images CI` push trigger:** the `paths-ignore` is gone. The
`discover` job now decides relevance for pushes the same way it already
did for pull requests.
- It diffs `github.event.before..HEAD` and skips the image builds for
docs/tests-only changes, while `images summary` still reports.
- A new branch (all-zero `before`) or a `before` commit the clone
doesn't have builds everything, and a failed diff still fails the gate.
- `main` and `master378` are not added to the push trigger: `main`
doesn't exist, and `master378` runs its own copy of this workflow.

## Verification

- The `discover` script was extracted and run locally for push events:

  | Push | `relevant_changes` |
  | --- | --- |
  | docs-only | `false` (images skipped, summary reports) |
  | workflow change | `true` |
  | new branch | `true` |
  | unknown `before` | `true` |

- The CI-related `Opc.Ua.Tools.Tests` (`~Ci`, 53 tests, including the
fail-closed diff pin) pass on net10.0.

## Cost

Overlapping merges to `master` now run CI and image builds in parallel
instead of cancelling each other, which uses more runner minutes on busy
days. Image pushes from overlapping `master` runs can finish out of
order, so `latest-master` reflects whichever finishes last. Version tags
are per build and unaffected.

## Related Issues

- Blocks cutting `release/2.0` for `2.0.0-preview.6` (see #4574).

## 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready Ready to merge once CI Passes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants