Files
UA-.NETStandard/samples/PubSub/ConsoleReferencePubSubClient
Copilot e78c648295 Images CI: publish samples once natively and stop compiling under QEMU (#4679)
# Description

Images CI took 45–100 min per image. The arm64 half of every
`linux/amd64,linux/arm64` build ran `dotnet restore` and `dotnet
publish` in an arm64 SDK container under QEMU. For `refserver`, publish
took 8 min natively for amd64 and 92 min emulated for arm64. On top of
that, every one of the 10 matrix legs recompiled the whole stack.

The output is framework-dependent portable IL, so the compile is now
done once, natively. The image legs only copy files onto the
per-platform runtime base image.

### Dockerfiles (all 10)
- **SDK stage runs on the build platform.** The `build` stage is pinned
to `--platform=$BUILDPLATFORM`. `-p:UseAppHost=false` keeps an x64 app
host out of the arm64 image.
- **New `prebuilt` stage.** It is chosen with a global `ARG
PUBLISH_SOURCE`, which defaults to `build`. Local `docker build` and the
`docker-compose` files are unchanged.
  ```dockerfile
  FROM scratch AS prebuilt
  ARG PUBLISH_DIR=artifacts/docker/publish
  COPY ${PUBLISH_DIR}/ /app/publish/

  FROM ${PUBLISH_SOURCE} AS publish
  ```
- **Pump: final-stage `RUN`s moved to a native stage.** The `sed` on
`openssl.cnf` and the `mkdir`/`chown`/`chmod` steps now run in a
`$BUILDPLATFORM` `layout` stage. The final stage copies `/layout/` to
`/`, so `/app` and `/diag` keep the `$APP_UID:0` owner and `g=u` mode
they had before. `openssl.cnf` therefore comes from the build-platform
base image. It is a plain config file, so it is expected to match
between architectures.

### `docker-image.yml`
- **Single image list.** The image list lives once, in `env.IMAGES`
(image, project, Dockerfile). `discover` emits it as the matrix.
- **New `publish-samples` job.** It runs `dotnet publish` for each
sample sequentially, so shared libraries compile once. It uses the same
options and NBGV version properties as the Dockerfiles and uploads the
output as the `docker-publish` artifact.
- **`build-and-push-image` legs.** Each leg:
  - downloads the artifact;
- builds with `PUBLISH_SOURCE=prebuilt` and
`PUBLISH_DIR=artifacts/docker/publish/<image>`;
  - no longer uses `setup-qemu-action`;
  - is named `build-and-push-image (<image>)`.
- **`images summary`.** It now also fails when `publish-samples` fails.

### Docs
- `ContainerReferenceServer.md` describes the publish-once / copy-only
flow and the build arguments.
- `DeveloperGuide.md` mentions the `publish-samples` job in the Images
CI section.

Expected: one native publish of about 7 min, then 10 parallel
copy-and-push legs of a few minutes each. Not yet confirmed on CI: the
sandbox could not pull MCR base images, so the prebuilt multi-platform
builds were checked against a Debian-based stand-in base, and the
build-from-source path was not built.

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

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: romanett <7413710+romanett@users.noreply.github.com>
2026-10-06 14:21:44 +02:00
..

Console Reference PubSub

A single, self-contained OPC UA Part 14 PubSub reference application built on the fluent PubSubApplicationBuilder + dependency injection + .NET Generic Host surface. One executable exposes three command-line-selectable modes, and publishes as a NativeAOT-ready single-file executable.

Modes

ConsoleReferencePubSubClient <mode> [options]
Mode Purpose
publisher Publishes a built-in sample DataSet over UDP/UADP or MQTT (UADP/JSON).
subscriber Receives DataSets and logs each decoded message to the console.
external Bridges an external OPC UA server to PubSub via the Opc.Ua.PubSub.Adapter library (publisher / subscriber / responder direction).

publisher

ConsoleReferencePubSubClient publisher --profile udp-uadp --interval 1000
ConsoleReferencePubSubClient publisher --profile mqtt-json --endpoint mqtt://localhost:1883

Options: --profile udp-uadp|mqtt-uadp|mqtt-json, --config-file <xml>, --publisher-id, --writer-group-id, --data-set-writer-id, --endpoint, --interval.

subscriber

ConsoleReferencePubSubClient subscriber --profile udp-uadp

Options: --profile, --config-file <xml>, --publisher-id-filter, --writer-group-id-filter, --data-set-writer-id-filter, --endpoint.

Publisher and subscriber on one computer

The default UDP endpoint, opc.udp://239.0.0.1:4840, is a multicast address. Multicast loopback is disabled by default, so a subscriber on the publisher's computer can miss the messages. To run both on one computer, give both the unicast loopback endpoint:

ConsoleReferencePubSubClient subscriber --profile udp-uadp --endpoint opc.udp://127.0.0.1:4840
ConsoleReferencePubSubClient publisher --profile udp-uadp --endpoint opc.udp://127.0.0.1:4840

See Run a first publisher and subscriber for the expected output and the settings that must match.

external

Bridges an external OPC UA server (defaults to the repository's ConsoleReferenceServer at opc.tcp://localhost:62541/Quickstarts/ReferenceServer; override with --endpoint or the OPCUA_EXTERNAL_ENDPOINT environment variable).

# Read an external server and publish its values (cyclic Read each cycle)
ConsoleReferencePubSubClient external --mode publisher --read-mode cyclic

# Read via a client Subscription cache, one subscription per WriterGroup
ConsoleReferencePubSubClient external --mode publisher --read-mode subscription --affinity writergroup

# Write received PubSub values back to an external server
ConsoleReferencePubSubClient external --mode subscriber

# Map an inbound PubSub Action to an external server method call
ConsoleReferencePubSubClient external --mode responder

# Run a bidirectional bridge in one process
ConsoleReferencePubSubClient external --mode publisher,subscriber

Options: --mode publisher|subscriber|responder (comma-separated list accepted), --read-mode cyclic|subscription, --affinity writergroup|datasetwriter, --endpoint <external server>, --pubsub-endpoint <udp url>, --hot-reload.

Hot reload

Add --hot-reload to the external bridge to run the opt-in live reconfiguration demo:

ConsoleReferencePubSubClient external --mode publisher,subscriber --hot-reload

The bridge writes pubsub-config.xml next to the executable before starting and loads adapter options from the copied appsettings.json in the same directory. Edit and save either file while the bridge is running:

  • appsettings.json: change ExternalPublisher:ReadMode from Cyclic to Subscription, or change ExternalPublisher:Affinity to DataSetWriter, to rewire the external-server publisher options.
  • pubsub-config.xml: add or remove a DataSetWriter to change the PubSub topology. The watching XML store raises a configuration change and the adapter incrementally rewires the affected binding.

The samples connect to the external server unsecured (SecurityMode.None) for zero-config interop. A production bridge must use SignAndEncrypt with a provisioned application instance certificate. See docs/PubSub.md external-server adapter section.

Build / publish

dotnet build samples/PubSub/ConsoleReferencePubSubClient/ConsoleReferencePubSubClient.csproj
dotnet publish samples/PubSub/ConsoleReferencePubSubClient/ConsoleReferencePubSubClient.csproj -r win-x64

See docs/PubSub.md for the full PubSub guide.