Skip to content

Commit a9ec0eb

Browse files
romanettclaudeCopilot
authored
Switch the preview feed to GitHub Packages and adopt the DI config-XML loading (#808)
* Switch the preview feed to GitHub Packages and adopt the DI config-XML loading The ADO opcua-preview feed is retired: Nuget.Config now points at the OPC Foundation GitHub Packages feed, the CI pipelines authenticate it with the GITHUB_PACKAGES_TOKEN secret through VSS_NUGET_EXTERNAL_FEED_ENDPOINTS, and the README documents the read:packages token a local restore needs. Every OPC UA package reference moves from 2.0.0-preview.2 to 2.0.245.28872-preview - the latest set published to GitHub Packages - and is pinned in one place, the OpcUaNetStandardVersion property in targets.props. The server samples hand their *.Config.xml to the hosted server of the stack (services.AddOpcUa().AddServer(configurationFile), issue #794, unblocked by OPCFoundation/UA-.NETStandard#4325): AddSampleServer gains configuration-file overloads which run the container's server instance through IOpcUaServerFactory, capture the loaded configuration for the forms, attach the log file the configuration names, and gate the start of the host on the server actually listening. The client samples and the ApplicationInstance-centric legacy samples name their configuration file directly instead of through ConfigSectionName, and the dead OPC UA sections leave the App.config files. New HostedServerBootstrapTests pin the bootstrap contract. Fixes #794 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Authenticate the CodeQL restore and fail the pipelines fast without a feed token The first CI run answered the GitHub Packages feed switch with a wall of 401s. Three fixes: The CodeQL workflow authenticates the feed with its own workflow token - reading a public package needs any authenticated principal, so this job needs no secret at all. The token is written into the checked-out Nuget.Config, which both solution restores read. The Azure pipelines cannot mint a GitHub token themselves, so they still need the GITHUB_PACKAGES_TOKEN secret pipeline variable. A probe step in front of every restore now turns a missing or rejected token into one readable error pointing at the README instead of hundreds of 401 lines. The endpoint list moves from a nested job variable into a direct env mapping on each restore step - the form documented for secrets, with one less expansion to reason about. The first run proved the credential provider picks the endpoint up either way. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move to the 2.0.262.32744-preview packages The latest set the nuget-publish workflow pushed to GitHub Packages from master, which the whole solution builds against without a source change. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Park the StateMachines expectations which need an SDK newer than 2.0.262 The StateMachines sample merged from master was validated against the nuget.org 2.0.0-preview.4 cut, which contains UA-.NETStandard #4368 (Executable/UserExecutable reporting for state machine causes, and the CurrentState/Id that goes with it). That change landed one master build after 2.0.262.32744-preview - the newest set on GitHub Packages, because every later publish fails on an unrelated signing error in the stack (Opc.Ua.WotCon.Bindings declares InternalsVisibleTo without a public key, which only the strong-name-signed publish build rejects). The three affected expectations go into the known-issue machinery the suite has for exactly this: the CurrentState/Id asserts of the node manager fixture into KnownIssue wrappers, the cause-offering client test into a subscription known-issue list mirroring SampleClientTests. All of them fail the moment a newer package set makes them pass, which is the reminder to remove them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move to the 2.0.280.50326-preview packages and lift the StateMachines park The stack's signed publish is repaired, so GitHub Packages carries master builds again; the newest set contains UA-.NETStandard #4368, which lets the StateMachines known-issue entry and its TESTING.md note pay out. The onboarding ticket surface moved from byte arrays to ArrayOf<ByteString> in and ArrayOf<StatusCode> out, which the device onboarding dialog and the GDS client test follow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Qualify the registrar methods with the Onboarding namespace, re-park StateMachines Two findings from running the merged tree on the 2.0.280 packages: The generated OnboardingClient now calls the type-declaration MethodId first and falls back to a browse path qualified with the Part 21 Onboarding namespace - the ns=0 browse names the hand-built registrar carried were the old contract, and GetTickets answered BadMethodInvalid. The node manager now brings the Onboarding namespace into the server table and creates the two Method BrowseNames in it, which is the shape the stack's own OnboardingClientTests pin. The StateMachines cause-offering test fails identically on master's CI at 2.0.0-preview.4, so the earlier park was removed for the wrong reason: this is not package lag but a defect in how the sample and the stack report Executable for causes. The known-issue entry returns with that corrected reason and pays out when the defect is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restore the sample table rows the merge dropped from the README The NodeManagement, RoleManagement and StateMachines rows of the workshop sample table were lost in an earlier merge resolution; the table matches master again, and only the preview-feed section differs on this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Align the registrar with the dual-generation design of #859 The type-declaration MethodIds of the companion model serve the current OnboardingClient's direct call - one roundtrip cheaper than the browse fallback the previous fix on this branch relied on - and the ns=0 BrowseNames keep the earlier client generation working. Same file as the master fix, so the next merge is conflict free. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Take the registrar ids from the generated Onboarding model constants The packages this branch builds against ship the generated model, so the namespace and the two type-declaration MethodIds come from Opc.Ua.Onboarding.Namespaces and Opc.Ua.Onboarding.MethodIds instead of spelled-out literals. The master variant (#859) keeps the literals until the packages move past 2.0.0-preview.4, which does not contain the generated model. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Use opcfoundation-org for package feed secret Co-authored-by: romanett <7413710+romanett@users.noreply.github.com> * Authenticate GitHub Packages via the Github-Feed service connection Replace the GITHUB_PACKAGES_TOKEN secret pipeline variable with the Github-Feed NuGet service connection: NuGetAuthenticate@1 now references the connection in all three pipeline templates, which removes the per-restore VSS_NUGET_EXTERNAL_FEED_ENDPOINTS endpoint JSON and the fail-fast token probe steps. The README documents how the service connection is set up instead of the secret variable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
1 parent cadd17a commit a9ec0eb

11 files changed

Lines changed: 104 additions & 33 deletions

File tree

‎.azurepipelines/ci.yml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,14 @@ jobs:
2828
pool:
2929
vmImage: $(poolImage)
3030
steps:
31+
# The OPC UA preview packages come from the OPC Foundation GitHub Packages
32+
# feed, which requires authentication even though it is public. The
33+
# Github-Feed NuGet service connection holds the credentials (a GitHub
34+
# personal access token with the read:packages scope) for the feed URL
35+
# declared in Nuget.Config, see 'Preview NuGet feed' in README.md.
3136
- task: NuGetAuthenticate@1
37+
inputs:
38+
nuGetServiceConnections: 'Github-Feed'
3239
- task: NuGetToolInstaller@1
3340
inputs:
3441
versionSpec: '>=7.0.x'

‎.azurepipelines/sln.yml‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33
#
44
jobs:
55
- job: buildallsln
6-
displayName: Build Solutions
6+
displayName: Build Solutions
77
pool:
88
vmImage: 'windows-2025-vs2026'
99
steps:
10+
# see ci.yml: the Github-Feed NuGet service connection authenticates the
11+
# OPC Foundation GitHub Packages feed declared in Nuget.Config.
1012
- task: NuGetAuthenticate@1
13+
inputs:
14+
nuGetServiceConnections: 'Github-Feed'
1115
- task: UseDotNet@2
1216
displayName: 'Install .NET 10.0'
1317
inputs:

‎.azurepipelines/test.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,11 @@ jobs:
1818
DOTNET_CLI_TELEMETRY_OPTOUT: true
1919
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true
2020
steps:
21+
# see ci.yml: the Github-Feed NuGet service connection authenticates the
22+
# OPC Foundation GitHub Packages feed declared in Nuget.Config.
2123
- task: NuGetAuthenticate@1
24+
inputs:
25+
nuGetServiceConnections: 'Github-Feed'
2226
- task: UseDotNet@2
2327
displayName: 'Install .NET 10.0'
2428
inputs:

‎.github/workflows/codeql-analysis.yml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ jobs:
2424
permissions:
2525
actions: read
2626
contents: read
27+
# read the OPC UA preview packages from the OPC Foundation GitHub
28+
# Packages feed declared in Nuget.Config
29+
packages: read
2730
security-events: write
2831

2932
strategy:
@@ -57,6 +60,13 @@ jobs:
5760
- name: Setup MSBuild.exe
5861
uses: microsoft/setup-msbuild@v3.0.0
5962

63+
# The preview packages come from the OPC Foundation GitHub Packages feed,
64+
# which requires authentication even though it is public. The workflow
65+
# token is enough to read public packages; the credentials are written
66+
# into the checked-out Nuget.Config, which the restores below read.
67+
- name: Authenticate GitHub Packages
68+
run: dotnet nuget update source opcfoundation-github --username "${{ github.actor }}" --password "${{ secrets.GITHUB_TOKEN }}" --store-password-in-clear-text --configfile Nuget.Config
69+
6070
- name: Restore Packages
6171
run: |
6272
nuget restore "UA Quickstart Applications.sln"

‎Nuget.Config‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,10 @@
33
<packageSources>
44
<clear/>
55
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
6+
<!-- Preview packages of the OPC UA .NET Standard stack, published by the
7+
nuget-publish workflow of OPCFoundation/UA-.NETStandard on every master
8+
build. GitHub Packages requires authentication even for public feeds:
9+
use a personal access token with read:packages scope, see README.md. -->
10+
<add key="opcfoundation-github" value="https://nuget.pkg.github.com/OPCFoundation/index.json" protocolVersion="3" />
611
</packageSources>
712
</configuration>

‎README.md‎

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -126,13 +126,36 @@ These paired client/server samples each demonstrate a specific OPC UA feature se
126126
## Getting Started
127127
All the tools you need for .Net Standard come with the .Net Core tools. See [here](https://docs.microsoft.com/en-us/dotnet/articles/core/getting-started) for what you need.
128128

129-
## Preview NuGet packages
129+
## Preview NuGet feed
130130

131-
The samples build against the preview packages of the OPC UA .NET Standard stack from the
132-
public [nuget.org](https://www.nuget.org/packages/OPCFoundation.NetStandard.Opc.Ua/) feed -
133-
no authentication or extra package source is needed. The version the samples currently build
134-
against is pinned in a single place, the `OpcUaNetStandardVersion` property in
135-
[targets.props](targets.props).
131+
The samples build against the preview packages of the OPC UA .NET Standard stack, which
132+
[the nuget-publish workflow](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/.github/workflows/nuget-publish.yml)
133+
publishes to the [GitHub Packages feed of the OPC Foundation organization](https://github.com/orgs/OPCFoundation/packages)
134+
on every master build. The feed is declared in [Nuget.Config](Nuget.Config):
135+
136+
```
137+
https://nuget.pkg.github.com/OPCFoundation/index.json
138+
```
139+
140+
GitHub Packages requires authentication even for public feeds. To restore locally, create a
141+
[personal access token](https://github.com/settings/tokens) with the `read:packages` scope and store
142+
it for the feed once (in your user-level NuGet configuration, not in the repository):
143+
144+
```
145+
dotnet nuget update source opcfoundation-github --username <your-github-username> --password <your-token> --store-password-in-clear-text
146+
```
147+
148+
Alternatively add the credentials to `%AppData%\NuGet\NuGet.Config` (Windows) or
149+
`~/.nuget/NuGet/NuGet.Config` (Linux/macOS) with the same `opcfoundation-github` key.
150+
151+
The [Azure DevOps pipelines](.azurepipelines) authenticate through the `Github-Feed` NuGet
152+
service connection of the Azure DevOps project, which the `NuGetAuthenticate` task hands to
153+
the restore steps. The service connection is of type NuGet with basic authentication: the
154+
feed URL must match the one in [Nuget.Config](Nuget.Config) exactly, the username can be
155+
anything, and the password is a token with the `read:packages` scope as above.
156+
157+
The version the samples currently build against is pinned in a single place, the
158+
`OpcUaNetStandardVersion` property in [targets.props](targets.props).
136159

137160
## Debugging the Opc.Ua.Core Nuget packages
138161

‎Samples/GDS/ClientControls/Controls/DeviceOnboardingDialog.cs‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -264,29 +264,29 @@ private async Task CallRegistrarAsync(bool register)
264264
{
265265
var client = new OnboardingClient(m_session, m_registrarNodeId, m_telemetry);
266266

267-
byte[][] tickets = new byte[m_tickets.Count][];
267+
ByteString[] tickets = new ByteString[m_tickets.Count];
268268

269269
for (int ii = 0; ii < m_tickets.Count; ii++)
270270
{
271-
tickets[ii] = m_tickets[ii].Ticket;
271+
tickets[ii] = m_tickets[ii].Ticket.ToByteString();
272272
}
273273

274-
int[] results = register
274+
ArrayOf<StatusCode> results = register
275275
? await client.RegisterTicketsAsync(tickets)
276276
: await client.UnregisterTicketsAsync(tickets);
277277

278278
for (int ii = 0; ii < TicketsListView.Items.Count; ii++)
279279
{
280-
TicketsListView.Items[ii].SubItems[2].Text = ii < results.Length
281-
? new StatusCode((uint)results[ii]).ToString()
280+
TicketsListView.Items[ii].SubItems[2].Text = ii < results.Count
281+
? results[ii].ToString()
282282
: "---";
283283
}
284284

285285
StatusLabel.Text = String.Format(
286286
CultureInfo.CurrentCulture,
287287
"{0} {1} ticket(s).",
288288
register ? "Registered" : "Unregistered",
289-
results.Length);
289+
results.Count);
290290
}
291291
catch (Exception exception)
292292
{

‎Samples/GDS/Common/DeviceRegistrarNodeManager.cs‎

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -82,14 +82,17 @@ public class DeviceRegistrarNodeManager : AsyncCustomNodeManager
8282
private readonly ITicketStore m_ticketStore;
8383

8484
/// <summary>
85-
/// Creates the node manager over the supplied ticket store.
85+
/// Creates the node manager over the supplied ticket store. The Onboarding
86+
/// companion model namespace is the manager's second namespace, because the two
87+
/// Method nodes carry the model's type-declaration NodeIds - see
88+
/// <see cref="CreateTicketMethod"/>.
8689
/// </summary>
8790
/// <exception cref="ArgumentNullException"><paramref name="ticketStore"/> is <c>null</c>.</exception>
8891
public DeviceRegistrarNodeManager(
8992
IServerInternal server,
9093
ApplicationConfiguration configuration,
9194
ITicketStore ticketStore)
92-
: base(server, configuration, NamespaceUri)
95+
: base(server, configuration, NamespaceUri, Opc.Ua.Onboarding.Namespaces.OpcUaOnboarding)
9396
{
9497
m_ticketStore = ticketStore ?? throw new ArgumentNullException(nameof(ticketStore));
9598
}
@@ -110,8 +113,14 @@ public override async ValueTask CreateAddressSpaceAsync(
110113
};
111114
#pragma warning restore CA2000
112115

113-
MethodState register = CreateTicketMethod(registrar, "RegisterTickets");
114-
MethodState unregister = CreateTicketMethod(registrar, "UnregisterTickets");
116+
MethodState register = CreateTicketMethod(
117+
registrar,
118+
"RegisterTickets",
119+
Opc.Ua.Onboarding.MethodIds.DeviceRegistrarAdminType_RegisterTickets);
120+
MethodState unregister = CreateTicketMethod(
121+
registrar,
122+
"UnregisterTickets",
123+
Opc.Ua.Onboarding.MethodIds.DeviceRegistrarAdminType_UnregisterTickets);
115124

116125
registrar.AddChild(register);
117126
registrar.AddChild(unregister);
@@ -139,21 +148,29 @@ public override async ValueTask CreateAddressSpaceAsync(
139148
/// </summary>
140149
/// <remarks>
141150
/// <para>
142-
/// Two details are dictated by the SDK rather than by Part 21. The BrowseNames are
143-
/// created in namespace 0, because <c>OnboardingClient</c> resolves the two Methods
144-
/// with an ns=0 browse path and finds nothing otherwise. And the <c>Tickets</c>
151+
/// Three details are dictated by the SDK rather than by Part 21, and they let both
152+
/// client generations call the Methods. The NodeId is the type-declaration MethodId
153+
/// of the Onboarding companion model, because the current generated
154+
/// <c>OnboardingClient</c> calls that id on the wrapped instance directly - which
155+
/// OPC UA Part 4 permits - and this hand-built registrar has no instantiated type to
156+
/// answer for it otherwise. The BrowseName stays in namespace 0, because the earlier
157+
/// <c>OnboardingClient</c> generation resolves the Methods with an ns=0 browse path.
158+
/// And the <c>Tickets</c>
145159
/// argument is declared as a <c>ByteString</c> array rather than as the Part 21
146160
/// <c>EncodedTicket</c> alias: <c>MethodState</c> type-checks every input argument
147161
/// against the declared DataType, and ns=0 models <c>EncodedTicket</c> with
148162
/// <c>String</c> as its base type, so the ByteString array both halves of the SDK
149163
/// actually exchange would be rejected with <c>Bad_TypeMismatch</c>.
150164
/// </para>
151165
/// </remarks>
152-
private MethodState CreateTicketMethod(NodeState parent, string name)
166+
private MethodState CreateTicketMethod(
167+
NodeState parent,
168+
string name,
169+
ExpandedNodeId typeMethodId)
153170
{
154171
#pragma warning disable CA2000 // Justification: Node ownership is transferred to the server address space.
155172
var method = new MethodState(parent) {
156-
NodeId = new NodeId(name, NamespaceIndex),
173+
NodeId = ExpandedNodeId.ToNodeId(typeMethodId, SystemContext.NamespaceUris),
157174
BrowseName = new QualifiedName(name),
158175
DisplayName = new LocalizedText(name),
159176
ReferenceTypeId = ReferenceTypeIds.HasComponent,

‎Tests/SampleClients.Tests/GdsClientTests.cs‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -536,24 +536,24 @@ private static async Task RegisterAnOnboardingTicketAsync(
536536
ExpandedNodeId.ToNodeId(registrar.NodeId, gdsClient.Session.NamespaceUris),
537537
NullTelemetry.Instance);
538538

539-
byte[][] tickets = [[1, 2, 3, 4]];
539+
ByteString[] tickets = [new byte[] { 1, 2, 3, 4 }.ToByteString()];
540540

541-
int[] registered = await client.RegisterTicketsAsync(tickets, ct).ConfigureAwait(true);
541+
ArrayOf<StatusCode> registered = await client.RegisterTicketsAsync(tickets, ct).ConfigureAwait(true);
542542

543-
Assert.That(registered, Has.Length.EqualTo(1), "RegisterTickets did not report a result per ticket.");
543+
Assert.That(registered.Count, Is.EqualTo(1), "RegisterTickets did not report a result per ticket.");
544544
Assert.That(
545-
StatusCode.IsGood(new StatusCode((uint)registered[0])),
545+
StatusCode.IsGood(registered[0]),
546546
Is.True,
547547
"The registrar rejected the ticket.");
548548

549549
phase.Enter("removing the onboarding ticket again");
550550

551-
int[] removed = await client.UnregisterTicketsAsync(tickets, CancellationToken.None)
551+
ArrayOf<StatusCode> removed = await client.UnregisterTicketsAsync(tickets, CancellationToken.None)
552552
.ConfigureAwait(true);
553553

554-
Assert.That(removed, Has.Length.EqualTo(1), "UnregisterTickets did not report a result per ticket.");
554+
Assert.That(removed.Count, Is.EqualTo(1), "UnregisterTickets did not report a result per ticket.");
555555
Assert.That(
556-
StatusCode.IsGood(new StatusCode((uint)removed[0])),
556+
StatusCode.IsGood(removed[0]),
557557
Is.True,
558558
"The registrar did not remove the ticket it had just accepted.");
559559
}

‎docs/TESTING.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -322,7 +322,8 @@ is what every other sample in the repository uses.
322322
`s_knownIssues` in `SampleServerTests` (and the same list in `SampleClientTests`) reports a
323323
listed sample as **ignored** rather than failed, and fails the moment it starts working, so
324324
an entry cannot rot. Both lists are used sparingly: a sample that is broken is worth fixing,
325-
not parking. Both lists are currently empty.
325+
not parking. Both lists are currently empty - the StateMachines subscription failure that
326+
was briefly parked turned out to be the test pressing the wrong cause, fixed for good.
326327

327328
## What Tier 1.5 checks today
328329

0 commit comments

Comments
 (0)