# 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>
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: changeExternalPublisher:ReadModefromCyclictoSubscription, or changeExternalPublisher:AffinitytoDataSetWriter, to rewire the external-server publisher options.pubsub-config.xml: add or remove aDataSetWriterto 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 useSignAndEncryptwith 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.