Skip to content

[2.0] Split the SDK into Ably.PubSub.Core and Device/Server door packages - #1340

Merged
sacOO7 merged 180 commits into
mainfrom
integration/v2
Nov 2, 2026
Merged

sacOO7 merged 180 commits into
mainfrom
integration/v2

Conversation

@sacOO7

@sacOO7 sacOO7 commented Sep 21, 2026 •

Copy link
Copy Markdown
Contributor

Summary

This PR merges integration/v2 into main. It is Ably Pub/Sub .NET 2.0: the single ably.io package is split into an internal core and two "door" packages (PDR-091b), so every client is classified as device-side or server-side on the wire. Ably's platform behaviour and MAU-based billing depend on that classification.

2.0 also moves the root namespace to Ably.PubSub, renames the REST client types to HTTP, removes the 1.x [Obsolete] members, adds protocol v6 support (publish results, message updates/deletes/appends, annotations, message retrieval) and replaces Environment/RestHost/RealtimeHost with Endpoint.

The branch is 167 commits ahead of main. Merge it only on the release owner's go, once every item in "Before merging" is done.

Packages

Package Audience Entry point
Ably.PubSub.Device Apps on end-user devices: desktop (WPF, WinForms), MAUI, .NET for Android/iOS, Unity PubSubDevice.CreateClient(...), which adds the ably-pubsub-device agent flag
Ably.PubSub.Server Backends: ASP.NET Core, workers, console jobs, functions PubSubServer.CreateRealtimeClient(...), PubSubServer.CreateHttpClient(...), which add the ably-pubsub-server agent flag
Ably.PubSub.Core None directly; a transitive dependency of both doors None. The client constructors are internal, so a client can only be created through a door.
ably.io (1.x) Existing consumers Maintenance only, from a v1 branch cut from tag 1.2.19: security and critical fixes for one year from the 2.0 release. No more ably.io releases from main.
  • The doors pin the core exactly ([$version$]), and the three packages are released together at one version.
  • The SDK identifier changes to ably-pubsub-dotnet/<version>. The side flags are versionless agent entries.
  • Unity ships one merged plugin, Ably.PubSub.Device.dll.

What's included

Package split (stacked PRs, each reviewed separately, followed by review-feedback commits on integration/v2):

Namespace and renames:

Protocol v6 (ECO-5754):

Other API changes:

Platform and packaging:

Release process:

Docs:

Tests:

Breaking changes for users

The full list is in the 2.0 section of CHANGELOG.md, and migration steps are in UPDATING.md. In short:

  • Install Ably.PubSub.Device or Ably.PubSub.Server, not ably.io, and create clients through the door factories.
  • Code changes:
    • the namespace moves from IO.Ably to Ably.PubSub;
    • REST types are renamed to HTTP;
    • the deprecated 1.x members are removed;
    • Endpoint replaces the old host options;
    • publish methods return a PublishResult.
  • Releasing a channel that isn't detached now throws.
  • Request()/RequestV2() without an X-Ably-Version header now get protocol-6 response shapes.
  • IHttpChannel and IRealtimeChannel gain members, so code that implements them (such as test fakes) must add them.
  • Device push-receive is not available in 2.0. Push.Activate()/Deactivate() throw an error that links Re-introduce device push notification support (FCM/APNs) for modern .NET Android/iOS and MAUI #1360. The decision record is PushNotifications.md.
  • Push administration (Push.Admin) is available in both packages. It is documented as server-side use that needs the push-admin capability.
  • v1 (IO.Ably.*) and v2 (Ably.PubSub.*) types no longer collide, apart from IO.Ably.DeltaCodec. A mixed dependency graph compiles, but UPDATING.md says to keep it temporary.

Still open into integration/v2

Before merging

Merge mechanics

Squash-merge only. The branch history contains a 1.6 MB test-results file (test-results/tests-netstandard-unit.trx, added and later removed) and several superseded builds of the Unity plugin DLL. Squashing keeps them out of main.

After the merge, release by hand per the "Release process" section of CONTRIBUTING.md. Publish only once this PR is on main: the README and CHANGELOG.md link to blob/main/UPDATING.md, which doesn't exist on main until then.

  1. Set the version in src/CommonAssemblyInfo.cs, unity/Assets/Ably/version.txt and the README examples. It is already 2.0.0, so this is needed only for a prerelease such as 2.0.0-rc.1.
  2. Regenerate Ably.PubSub.Device.dll from the release commit (the updater builds from CommonAssemblyInfo.cs). Then check it for duplicate attributes with monodis and run the Unity EditMode/PlayMode smoke tests.
  3. Pack with package.yml.
  4. Push Ably.PubSub.Core first. Once nuget.org lists it, push Ably.PubSub.Device, then Ably.PubSub.Server.
  5. Check how the README renders on the three nuget.org package pages.

After release

Reviewer notes

  • The diff is large and mostly mechanical (renames and moves). Each part was reviewed in the PRs listed above. Focus on these:
    • src/Ably.PubSub.Device, src/Ably.PubSub.Server, src/Ably.PubSub.Side/Side.cs and src/Ably.PubSub.Shared/Agent.cs: classification and agent flags.
    • nuget/*.nuspec: exact core pin and package contents.
    • .github/workflows/package.yml and the CONTRIBUTING.md release section.
    • CHANGELOG.md 2.0 and UPDATING.md for accuracy.
  • By decision, the user docs have no "experimental" labels and no notes about specific known failures.
  • CodeRabbit skipped this PR because of its size (over 800 files; its limit is 100).

umair-ably and others added 30 commits September 2, 2026 14:32
Per PDR-091b plan decisions 5b and 6:

- delete the `IO.Ably.Android` (monoandroid) and `IO.Ably.iOS` (Xamarin.iOS)
  platform heads. They were 30-line `Platform.cs` heads whose only functional
  difference from the netstandard2.0 head was reporting the agent runtime as
  `xamarin`, no test workflow covered them, and they are what pinned the
  packaging workflow to windows-2022 plus a legacy Xamarin VS installer.
  Classic Xamarin / .NET for Android+iOS / MAUI consumers now receive the
  netstandard2.0 asset.
- delete the `IO.Ably.Push.Android` / `IO.Ably.Push.iOS` satellites, their
  nuspecs and the `package-push` scripts. `ably.io.push.android` /
  `ably.io.push.ios` 1.x continue from the 1.x maintenance line; a modern
  net8.0-android/net8.0-ios port is a post-GA 2.x follow-up.
- delete the `AndroidSample` and `DotnetPush` examples (they target the removed
  heads and satellites; they live on in the 1.x maintenance line).
- delete `IO.Ably.PackagePush.sln`, `IO.Ably.Xamarin.sln` and
  `IO.Ably.DotNetPush.sln`, which only existed for the removed projects, and
  drop the removed projects from the remaining solutions.
- drop the ILRepack `_Package_Merge_All` task, the `PushPackage` target and the
  Xamarin build tasks from the Cake build. The ILRepack helper itself stays:
  `Update.AblyUnity` still uses it to build the Unity plugin assembly.
- drop the Android SDK and legacy Xamarin installer steps and the whole
  `package-push` job from `.github/workflows/package.yml`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per PDR-091b plan decision 2: the package/assembly identity changes, the code
namespace does not. `namespace IO.Ably` and every public type name are
untouched; the compiled assembly becomes `Ably.PubSub.Core.dll`.

The distinct assembly name is the load-bearing half of the decision: with
`IO.Ably.dll` kept, an app that transitively pulled both `ably.io` and the new
core would have MSBuild pick the higher version with only an MSB3277 warning
and fail at runtime. With a distinct assembly name and a shared namespace,
mixing the two produces loud CS0433 compile errors instead.

Moves (via `git mv`, so history follows):

  src/IO.Ably.NETStandard20      -> src/Ably.PubSub.Core
  src/IO.Ably.NETFramework       -> src/Ably.PubSub.Core.NETFramework
  src/IO.Ably.Shared             -> src/Ably.PubSub.Shared
  src/IO.Ably.Shared.MsgPack     -> src/Ably.PubSub.Shared.MsgPack
  src/IO.Ably.Tests.Shared       -> src/Ably.PubSub.Tests.Shared
  src/IO.Ably.Tests.DotNET       -> src/Ably.PubSub.Tests.DotNET
  src/IO.Ably.Tests.NETFramework -> src/Ably.PubSub.Tests.NETFramework
  src/IO.Ably.sln                -> src/Ably.PubSub.sln
  src/IO.Ably.Package.sln        -> src/Ably.PubSub.Package.sln
  src/IO.Ably.NetStandard.sln    -> src/Ably.PubSub.NetStandard.sln
  src/IO.Ably.NetFramework.sln   -> src/Ably.PubSub.NetFramework.sln

Target frameworks are unchanged: `netstandard2.0;net6.0;net7.0` plus the
`net462` head (decision 5).

Also updated: every project/solution/shared-project import path, the
`AssemblyName` and XML documentation file names on both heads, the
`InternalsVisibleTo` entries for the renamed test assemblies, `AssemblyTitle`,
the Cake paths/targets and the test-assembly names they look for, and
`tools/list-test-categories.fsx`.

`IO.Ably.ruleset`, `IO.Ably.Tests.ruleset`, `stylecop.json` and `IO.Ably.snk`
keep their names: the strong-name key must stay byte-identical and renaming the
rulesets buys nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ably.io` is never published from this branch again — 1.x fixes ship from the
1.x maintenance line — so `nuget/io.ably.nuspec` is deleted rather than
retargeted, and `nuget/ably.pubsub.core.nuspec` takes its place.

- `<id>Ably.PubSub.Core</id>`, same `$version$` token, authors/owners/license/
  icon/readme/repository metadata as before, copyright year bumped to 2026.
- The `<description>` states plainly that this is an internal implementation
  package for Ably's own `Ably.PubSub.Device` / `Ably.PubSub.Server` packages
  and is not intended for direct use (PDR-091b §1). NuGet consumers receive it
  transitively, so "internal" is a statement in metadata and docs, not
  something the dependency graph can enforce.
- Dependency groups: netstandard2.0 / net6.0 / net7.0 / net46, each declaring
  Newtonsoft.Json 13.0.1 and System.Threading.Channels. The monoandroid /
  Xamarin.iOS / monotouch groups are gone with their heads.
- The net46 assets now come from the plain build output rather than an
  ILRepack-merged `Packaged/` directory, hence Newtonsoft.Json is a declared
  net46 dependency instead of a merged-in one.
- `IO.Ably.DeltaCodec.*` now needs explicit `files` entries: the old
  `IO.Ably.*` glob happened to pick the delta codec up, and
  `Ably.PubSub.Core.*` does not.

`_Package_Create_NuGet` now packs a list of nuspecs so stack PR 2 can add the
device and server nuspecs to it, and `package.cmd` / `package-unity.sh` no
longer name `ably.io`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Ably.PubSub.Core` starts at 2.0.0 and, from stack PR 2 on, releases in
lockstep with `Ably.PubSub.Device` and `Ably.PubSub.Server`. Bump the single
source of the runtime version (`src/CommonAssemblyInfo.cs`, read back by
`Defaults.GetVersion()`) and the Unity package version, and move the copyright
year to 2026.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Unity package ships a single ILRepack-merged plugin assembly. Point
everything that names it at the new assembly name: the `Update.AblyUnity` Cake
task (input and output), `link.xml`'s `<assembly fullname>`, the
`precompiledReferences` of the AblySandbox / EditMode / PlayMode asmdefs, and
the `unity/CONTRIBUTING.md` note.

The stale `unity/Assets/Ably/Plugins/IO.Ably.dll` is removed rather than
regenerated: ILRepack needs Mono or Windows, neither of which is available in
the environment this branch was prepared in, so the merged
`Ably.PubSub.Core.dll` has to be produced by running
`unity-plugins-updater.sh 2.0.0` (or the `.cmd`) on a machine that has Mono.
The plugin's `.meta` is renamed in place so the importer settings and asset
GUID survive; the regenerated dll drops straight into it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Minimal docs pass only; the full README rewrite and the migration guide are a
later phase of the plan.

- README: a "2.0 is in development" note at the top describing the new package
  set (`Ably.PubSub.Device` / `Ably.PubSub.Server` in the next PR,
  `Ably.PubSub.Core` internal), the unchanged `IO.Ably` namespace, the
  never-mix rule, and that `ably.io` 1.x continues from a maintenance branch.
  The "Supported platforms" table drops the Xamarin.Android/iOS/Mac rows for
  ".NET for Android, .NET for iOS and MAUI via netstandard2.0", and the MAUI
  `TrimmerRootAssembly` snippet now names `Ably.PubSub.Core`. Install and usage
  sections still describe 1.x and say so.
- CONTRIBUTING: the "working from source" project paths, and the release-step
  artifact names.
- cake-build/README: new project names, the `Build.Xamarin` target is gone, and
  the packaging section documents `package.cmd` producing
  `Ably.PubSub.Core.*.nupkg` from the nuspec list plus `package-unity.sh`.
- PushNotifications.md: a note that it describes the 1.x push-receive packages,
  which are not in the 2.0 set, and that push admin is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`IoC`'s static constructor located the platform head's `IO.Ably.Platform` type
by loading an assembly literally named "IO.Ably". After the assembly rename
that load throws `FileNotFoundException`, which the ctor swallows, leaving
`Platform` null. Every platform service then silently degraded: the transport
factory and `PlatformId` fell back (so the agent string lost its `dotnetN` /
OS entries), and `IoC.MobileDevice` threw `NullReferenceException` because only
`NotImplementedException` is caught there — which is what 18 `RestInitSpecs`
tests hit.

`Platform.cs` is compiled into the same assembly as this shared code by every
head, so look the type up in this assembly instead. That is also immune to any
later rename.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`FindTestAssemblies` defaults to a filename pattern, which still looked for
`IO.Ably.Tests.*.dll` and so found nothing after the test assemblies were
renamed. The Cake helper only warns when it finds no assemblies and the task
then returns early, so the .NET Framework test legs would have gone green
without running a single test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- D9: remove the committed 1.6 MB test-results .trx artifact and ignore *.trx and test-results/
- D27: add a pinned root nuget.config (nuget.org only, with source mapping) and drop the four solutions' dangling .nuget\NuGet.Config solution item
- D29: align built-against System.Threading.Channels to 4.6.0 (matching the nuspec) and System.Threading.Tasks.Extensions to 4.5.2 (matching the net46 head)
- D11: document the transitive ably.io/Ably.PubSub.* mixed-graph failure (CS0433, type identity) plus the dotnet-nuget-why detection and extern alias isolation recipe
- D32: replace the false "rewritten later in this stack" Installation/Usage promise with an honest before-2.0-GA sequencing note
- D33: discourage the direct core AblyRealtime/AblyRest constructors in XML docs (summary one-liner plus remarks rationale), pointing at the door factories
- D34: drop the stale 1.x ably.io NuGet badge from the README
- D35: fix stale doc cross-references (PushNotifications.md Dart link, unity/README.md dead anchor, past-dated protocol-v1 deprecation line)
- D17 (change 1): sign the net46 core head under package builds only, matching the SDK-style core
- D8 (guard): fail _Package_Unity packaging when the committed Unity plugin Ably.PubSub.Device.dll is missing

PubSub device/server split rollout (PDR-091b2) review follow-ups.
The core stamped `ably-dotnet/<version>` as its RSC7d1 family identifier.
At the Pub/Sub package split (PDR-091b) it becomes
`ably-pubsub-dotnet/<version>`, registered in ably-common#361 as a
versioned `sdk` entry.

The flip is the point: with the 1.x maintenance branch keeping the old
identifier, the string alone partitions legacy traffic from
new-package traffic, so no version correlation is needed to tell them
apart.

Updates the four spec files that assert on the token: the shared
realtime connection-parameter specs, the shared REST HTTP client specs,
and the Unity EditMode/PlayMode specs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ports the ably-js `packages/shared/side.ts` contract (ably-js#2293,
ably-java#1233).

`src/Ably.PubSub.Side/Side.cs` is a single shared source file compiled
into both door assemblies by a `<Compile Include>` item rather than
published as a third package - the .NET analogue of ably-js's
`shared/side.ts` and ably-java's `shared/.../Side.java`. It holds both
side identifiers, with the load-bearing-suffix warning, and
`WithSideAgent`, which:

  - builds a new `Agents` dictionary from the caller's entries, so the
    caller's own dictionary instance is never mutated;
  - applies the side entry last with a null value, so the flag is
    versionless (`Agent.AddAgentIdentifier` already emits a bare token
    for a null version) and wins a collision on its own key.

`PubSubServer` (`CreateRealtimeClient` / `CreateHttpClient`) and
`PubSubDevice` (`CreateClient`) each take the three shapes the core
constructors accept - key or token string, `ClientOptions`,
`Action<ClientOptions>` - and return the concrete `AblyRealtime` /
`AblyRest`. One device door by design: device-side connectionless
operations stay available on that client, so there is no device HTTP
door.

Both doors are strong-named in every configuration, so a consumer of
the signed core never mixes signed and unsigned Ably assemblies, and
both target the core's `netstandard2.0;net6.0;net7.0`. Server also gets
an old-style `net462` head mirroring the core's, so the package has a
`lib/net46` asset; there is deliberately no `net462` Device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ably.pubsub.device.nuspec` and `ably.pubsub.server.nuspec` carry the
same authors/licence/icon/repository metadata as the core's, and each
declares an exact pin on the core (`[$version$]`) per target-framework
group - the NuGet analogue of ably-js's exact `peerDependencies`, so a
consumer can never resolve two core versions. Server ships net46 (from
its .NET Framework head), netstandard2.0, net6.0 and net7.0; Device
ships the same minus net46.

`_Package_Create_NuGet` packs core, then device, then server, matching
the publish order stack PR 3 will need.

The `[$version$]` substitution inside a dependency version attribute is
marked TODO to verify on the release dry run - no nuget.exe or Mono
locally to check it. Stack PR 3 adds the pre-flight that asserts the
packed pin equals the version.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Thirteen specs in Ably.PubSub.Tests.DotNET/PubSub/PubSubPackageSpecs.cs.
They live in the .NET test head rather than Ably.PubSub.Tests.Shared
because Ably.PubSub.Device has no net462 asset, so the shared project
cannot reference it from the .NET Framework head.

What they pin, using the existing MockHttp/FakeHttpMessageHandler
capture for the header and the fake transport factory for the realtime
connection params, so nothing here opens a network connection:

  - the `Ably-Agent` header a door-created REST client actually puts on
    the wire contains the bare side flag, no versioned form of it, the
    versioned `ably-pubsub-dotnet/<version>` family identifier, and not
    the other side's flag;
  - the same for the realtime connection params from
    `TransportParams.GetParams()`;
  - caller `Agents` entries survive alongside the flag; a caller entry
    under the side's own key is overridden; the caller's own dictionary
    instance is untouched;
  - null options and a null configure action throw
    ArgumentNullException from every door;
  - all three overload shapes return the concrete core types;
  - the string overload's key-versus-token colon rule.

The identifiers are written out as literals, not read from `Side`, so a
rename of the constant fails here instead of passing silently.
Verified by mutation: giving the flag a version fails 8 of the 13.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the "added in the next PR in this stack" placeholders in the
2.0-in-development note with install commands and the door snippets for
both sides, and states plainly that the doors are the supported entry
points and Ably.PubSub.Core is internal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…evice door

- D3: add ClientOptions.Clone() to the core (member-wise copy; fresh dictionaries for Agents/AuthHeaders/AuthParams/TransportParams; private write-only host fields carried from inside the class) and make Side.WithSideAgent stamp a clone, never the caller's instance; rewrite the mutation-ratifying spec and add a cross-door reuse regression spec
- D4: add PubSubDoorSandboxSpecs (type=integration) - one sandbox publish/subscribe round-trip per realtime door and one REST call through the HTTP door
- D14: pin the unclassified-core promise - directly-constructed AblyRest/AblyRealtime emit neither side flag (while carrying the family identifier)
- D16: exception-parity spec - the no-credentials HTTP door fails with exactly the core's own 40106, proving it neither requires nor injects auth
- D28: fix Ably.PubSub.Package.sln shared-project metadata (drop phantom GUID, register the real Core consumer)
- D1: merge the Ably.PubSub.Device door into the Unity plugin - MergeDLLs gains an internalize-exclude-file parameter, cake-build/unity-internalize-exclude.txt keeps IO.Ably.PubSub.Device.* public, the merge output is renamed Ably.PubSub.Device.dll (.meta GUID preserved, link.xml, three test asmdefs, unity CONTRIBUTING/README synced), and the Unity sample constructs through PubSubDevice.CreateClient
- D36: route the NotificationsPublisher example through PubSubServer.CreateRealtimeClient with a server-door project reference
- D33: soft-[Obsolete] the five public core AblyRealtime/AblyRest constructors pointing at the door factories; scoped CS0618 suppressions at the sanctioned door call sites; NoWarn 618 on the two test heads that deliberately exercise the constructors
- D17 (changes 2-3): make door signing Package-conditional in lockstep with the core (all three door heads) and delete the ruleset CS8002 suppression so any future signed-vs-unsigned mix fails the build
- D32: rewrite the README Installation/Usage sections onto the door packages and drop the now-satisfied interim sequencing note; the transport-buffer example moves to the server door alongside the constructor obsoletion
- D15: verified locally with nuget.exe 6.3.1 (Mono) that $version$ substitutes inside dependency version attributes (packed pins read [2.0.0]); settled the TODO comments in both door nuspecs
- MergeDLLs also gains a Mono-facades /lib fallback (appended only when the facades directory exists on disk, so Windows invocations are bit-for-bit unchanged): under Mono, /targetplatform:v4 cannot resolve the netstandard facade the netstandard2.0 inputs reference

Includes the regenerated Unity plugin, produced by the sanctioned
./unity-plugins-updater.sh 2.0.0 pass under local Mono with the facades-lib
robustness fix; merged-assembly surface verified with monodis: assembly
Ably.PubSub.Device 2.0.0, door public, core public, Newtonsoft/DeltaCodec
internalized (D8, D1 dry-run items executed). Unity EditMode/PlayMode smoke
remains for the Windows/Unity pass.

PubSub device/server split rollout (PDR-091b2) review follow-ups.
Three packages ship at one version and both doors pin the core with an
exact version range. Nothing in the build enforced any of that, so a
loose pin, a stale build or a version-file skew would first become
visible to a consumer after the version had been published and could
never be changed again.

cake-build/tasks/release.cake adds three tasks that bracket the pack:

  _Release_Preflight       source-only assertions: the --version input
                           equals all three attributes in
                           src/CommonAssemblyInfo.cs and
                           unity/Assets/Ably/version.txt; nuget/ holds
                           exactly the lockstep set with the expected
                           ids and the $version$ token; every dependency
                           group of every door pins Ably.PubSub.Core as
                           [$version$]; nothing depends on ably.io or
                           the 1.x push satellites.
  _Release_Verify_Files    every <file src> glob resolves to a real
                           file. nuget silently omits a files entry it
                           cannot resolve, so this is the last point at
                           which an empty or partial package can be
                           caught.
  _Release_Verify_Packages post-pack assertions against the produced
                           .nupkg files: id, version, one lib/<tfm>
                           assembly per declared target, and the packed
                           door -> core pin.

The pre-flight is a dependency of _Version rather than of
_Package_Build_All because _Version overwrites CommonAssemblyInfo.cs
with the --version input; running after it would make the version
assertion a tautology. Package now fails loudly without --version
instead of silently producing nothing.

The packed-pin assertion is what proves the open question from the door
packages PR: NuGet does substitute $version$ inside a dependency's
version attribute, and the packed pin comes out as [2.0.0]. Both door
nuspecs lose their TODO and say so.

The same source-level assertions are duplicated as xunit specs in
Ably.PubSub.Tests.DotNET/PubSub/PackagingSpecs.cs, so a PR that edits a
nuspec fails the ordinary unit run without waiting for a packaging job.
They locate nuget/ by walking up from the test assembly rather than by
counting directories.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The lockstep invariants live in the version files and the nuspecs, none
of which the compiler reads, and a NuGet version once published can
never be changed. This packs all three packages from every PR commit
and runs the pre-flight and the post-pack assertions over them, so a
version-file skew, a loosened pin, a renamed id or a nuspec files entry
that no longer resolves fails on the PR that introduced it rather than
on release day.

Windows, not Linux, because the core and server packages carry a
lib/net46 asset built by an old-style MSBuild head. `dotnet pack
-p:NuspecFile=...` does pack a nuspec Mono-free, so packing is not what
needs Windows - the net462 build is - and _Release_Verify_Files fails
loudly rather than letting a Linux run ship packages silently missing
that asset.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Today's release is a human downloading an artifact zip and running
`dotnet nuget push` by hand. With three packages that must land in
dependency order, at one version, and where a half-finished release
cannot be undone, that is not a process anyone should be asked to
execute correctly under pressure.

publish.yml is dispatch-only, takes `version` and `dry_run` (default
true), and:

  - runs the pre-flight before anything is built, so a mismatched
    version or a loosened pin aborts the run with nothing produced;
  - builds, packs and creates the .unitypackage in one job from one
    build, so the Unity asset and the .nupkg files can never come from
    different builds of the same version;
  - pushes core -> device -> server, and waits for the core version to
    be listed on nuget.org before the doors that pin it go out, because
    nuget.org validates and indexes asynchronously;
  - queries the flat container before each push and skips a version
    already published, so a run that fails partway is completed by
    re-running it with the same version rather than by hand;
  - creates the tag and GitHub release, with all four artifacts
    attached, from a separate job so that contents: write is scoped to
    the only step that needs it;
  - prints the unlist guidance on every run, because NuGet cannot
    delete and the moment it is needed is the moment nobody wants to be
    reading a workflow file.

Authentication is nuget.org Trusted Publishing: GitHub OIDC exchanged
by NuGet/login for an API key valid for one hour, requested immediately
before the push, so the repository holds no long-lived NuGet
credential. The fallback to a NUGET_API_KEY secret is documented in a
comment at the step, and the registry-side setup in CONTRIBUTING.md.

Because the pre-flight demands the 2.0 nuspec set, this workflow is
inert on a 1.x checkout: it can be cherry-picked to the default branch
(which is where `gh workflow run` reads workflow files from) and
dispatched against integration refs without being able to release 1.x.

package.yml is deleted rather than kept alongside: it built the same
artifacts by the same Cake targets and then asked a human to push them,
which is now `dry_run=true` with assertions. package.cmd,
package-unity.sh and build.sh are unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CONTRIBUTING.md's release process was ten manual steps ending in a
hand-run `dotnet nuget push` of an artifact zip. It is now: bump one
version in two files, PR, merge, dispatch publish.yml with
dry_run=true, dispatch it again with dry_run=false. The tag, the GitHub
release and the .unitypackage attachment are the workflow's job. The
changelog and Headway steps are unchanged, and the re-run-to-complete
and unlist-to-abandon behaviours are written down where someone
mid-incident will find them.

Adds the nuget.org Trusted Publishing setup as its own section: what
the policy binds (owner, repository, the workflow file name only), that
its scope has to allow new packages if a brand-new id is being claimed,
that a private-repo policy is provisional for 7 days until the first
publish gives nuget.org the repository ids, that it matches by
repository name and so must be created after any rename, and how to
fall back to an API key.

cake-build/README.md documents Release.Preflight and
Release.VerifyPackages, the three packages that package.cmd now
produces, --packageOutput, and the `--` separator that Cake's reserved
--version flag forces.

README gains a paragraph on why the exact core pin means all
Ably.PubSub.* references in a project move together.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tooling

- D17 follow-up (composition break surfaced by the release dry run): under Package builds the signed core references the unsigned submodule-built DeltaCodec on the SDK TFMs - a pre-existing mix the deleted global ruleset suppression had been masking. Scope a CS8002 NoWarn to the core's Package-conditional signing group only (the doors compile clean and get nothing); the plain-Release guard stays fully live; proper cure is signing the submodule build (follow-up)
- D25: make 2.0.0-beta.N buildable and publishable - _Version stamps numeric identity into AssemblyVersion/AssemblyFileVersion (CS7034 otherwise) and the full string into AssemblyInformationalVersion; ReleaseAssertVersionFilesAgree asserts per-attribute expectations and its success message reworded; gh release create gains --prerelease for hyphenated versions; PackagingSpecs.TheTwoVersionFiles_Agree mirror-patched to the same split; README exact-pin paragraph gains the prerelease-pinning sentence
- D26: publish.yml gains a workflow-level concurrency group keyed on the version (never cancel in-progress) and the publish job runs in the `release` environment as the required-reviewer gate
- D7: new pre-flight assertion ReleaseAssertUnityPluginVersion - the committed unity/Assets/Ably/Plugins/Ably.PubSub.Device.dll must exist and carry the release's numeric identity and full informational version, so a stale merged plugin can never ship inside the .unitypackage
- D1 (CI): publish.yml regenerates the Unity plugin from the release checkout before UnityPackage and cross-checks identity/informational version against the committed DLL, which stays canonical and ships (it is what the Unity test pass validated)
- D12: nuspec projectUrl/releaseNotes/repository URLs moved to ably-pubsub-dotnet in all three nuspecs (metadata freezes per version at push time)
- D13: CHANGELOG gains the 2.0.0 entry and UPDATING.md is added as the 1.x -> 2.0 migration guide (door factories, mixed-graph rule, exact pinning, MAU forcing function, Xamarin push-receive gap, Unity door usage)
- D30: the five run-tests workflows also trigger on pushes to integration/** branches (previously only release-dry-run.yml did)
- D31: RestoreSolution stops swallowing restore failures - Windows NuGetRestore throws; the non-Windows nuget CLI path probes for the tool, warns-and-skips when absent (Mono-less macOS/Linux legs) and throws when present-and-failing; DotNetRestore now throws unconditionally
- D5: features.yml repository-name updated to ably-pubsub-dotnet - the check stays red regardless until the ops-side AWS IAM trust-policy re-bind allows the renamed repo
- D18 (optional polish, included - builds clean): Microsoft.SourceLink.GitHub 8.0.0 with PublishRepositoryUrl/EmbedUntrackedSources on the three SDK csprojs

Deferred/ops notes: the Windows-pack halves remain for CI (net46 pack/install
check, sn -v on packed assemblies, the publish.yml regenerate/cross-check dry
run); the `release` GitHub environment must be created and given required
reviewers plus a deployment-branch restriction in repo Settings - referencing
it auto-creates it unprotected; D13's entry needs its release date and compare
link when the release is cut; D25's wire-agent consequence (a beta build
reports ably-pubsub-dotnet/2.0.0 without the label via Defaults.GetVersion())
needs rollout-owner sign-off; Unity EditMode/PlayMode smoke of the committed
plugin remains for the Windows/Unity pass.

PubSub device/server split rollout (PDR-091b2) review follow-ups.
Bring the main-side changes now on integration/v2 (RTN23 connection
fixes, ably-dotnet -> ably-pubsub-dotnet rename references, the 1.2.19
release commits and the package-push runner fix) into the split-1 Core
restructure. Rename-aware resolution: main's edits to the old
src/IO.Ably.* paths land in the renamed src/Ably.PubSub.* files via
git rename detection (verified: TransportParams RTN23b heartbeats change
is byte-identical to main in the renamed Ably.PubSub.Shared file).

Conflict resolutions:
- .github/workflows/package.yml: keep restructure (the package-push
  Xamarin job was removed in the 2.0 layout; main's runner fix targets
  that now-deleted job).
- CONTRIBUTING.md, README.md: keep the 2.0 restructure content; main's
  ably-dotnet -> ably-pubsub-dotnet URL swaps are cosmetic and slated
  for a post-merge docs pass.
- src/CommonAssemblyInfo.cs, unity/Assets/Ably/version.txt: keep 2.0.0
  (main's 1.2.19 bump is superseded, not a downgrade of the 2.0 branch).
- nuget/io.ably*.nuspec, unity/Assets/Ably/Plugins/IO.Ably.dll: keep the
  restructure deletions (renamed to ably.pubsub.* / Ably.PubSub.Device).
Propagate the integration/v2 sync (RTN23 fixes, rename references,
1.2.19 release commits) up the stack via the split-1 Core branch.
Clean auto-merge, no conflicts.
Propagate the integration/v2 sync (RTN23 fixes, rename references,
1.2.19 release commits) to the top of the stack.

Conflict resolutions:
- .github/workflows/features.yml: both sides set
  repository-name: ably-pubsub-dotnet; keep the incoming explanatory
  comment about the IAM OIDC role binding.
- CHANGELOG.md: both sides added a section at the top; keep both, the
  hand-written 2.0.0 section above the generated 1.2.19 section.
M18: the door factory PubSubDevice.CreateClient clones the options it
is given, where new AblyRealtime(options) used to store them by
reference. The dashboard sample created its client at startup and
assigned _clientOptions.ClientId in the connect handler, so the typed
ClientId mutated only the caller's copy and was silently ignored by the
already-created client - the sample's ClientId input became a no-op.

Restructure the sample so ClientId is set before the options are
cloned, and recreate the client on each connect so a ClientId changed
between sessions is captured:
- InitializeAbly() now only constructs _clientOptions.
- New CreateAblyClient() creates the client, wires the existing
  Connection.On handler unchanged, and closes any client it replaces.
- ConnectClickHandler() closes when connected; otherwise it sets
  ClientId, recreates the client, and connects.

Scope extension over the planned AblyMain.cs-only change: AblyChannel
and AblyPresence captured the client reference in readonly fields at
startup, so recreate-on-connect requires them to accept an updated
reference - they gain an internal UpdateClient(AblyRealtime) which
AblyMain calls (null-conditionally) after each recreation.

unity/README.md's simpler sample (no connect handler) gets the minimal
faithful mapping: InitializeAbly split into options construction plus
CreateAblyClient, with a comment on the clone semantics.
Merge the door-packages branch (M18 sample fix) so the release-tooling
tip stays consistent with the layer below. Clean auto-merge.
[Split 3/3] Lockstep release pre-flight, release dry-run CI job, and publish workflow
Review follow-ups (M-IDs from the review-response plan):

- M5: FindTestAssemblies now throws when a pattern matches zero test
  assemblies instead of warning and returning green - a glob typo can
  no longer pass as a successful run with zero tests.
- M40: features.yml gains the integration/** push trigger the other
  six workflows already have.
- M7: the core nuspec lists the lib/net46 files explicitly (dll + xml
  + DeltaCodec dll) so the old glob no longer sweeps in the .dll.config
  and .pdb.
- M34: the net46 Server head cannot host the SourceLink analyzer (legacy
  packages.config project); the limitation is documented in the csproj
  header alongside the same-shaped core head rather than half-wired in.
- M32: CONTRIBUTING no longer claims the publish workflow uses no GitHub
  environment - it documents the release environment as the human
  approval gate and the optional nuget.org policy Environment pin.
- M37: release steps note that the hand-written 2.0.0 CHANGELOG section
  must be merged with the generator output, not duplicated.
- M38: the Unity regenerate-and-cross-check step documents its limit
  (identity/version comparison, not byte identity - ILRepack output is
  nondeterministic; the committed, Unity-tested copy stays canonical).
- M39: the same step documents why the pack step must stay ordered
  before it (Build.NetStandard depends on _Clean, which wipes bin/).
- M1 hardening: the committed Unity plugin path is defined once, in
  paths.cake (UnityPluginDll), and referenced by the build task, the
  packaging guard and the release pre-flight so the sites cannot drift.
- M2 residual: remaining ably-dotnet URLs and package-name mentions in
  README updated to ably-pubsub-dotnet.
- README usage sample: add the missing 'using IO.Ably;' - the block
  uses core types (ConnectionEvent, IRealtimeChannel) that the door
  namespace alone does not import, so it did not compile as copied.
  (UPDATING.md samples already carry both usings.)

Validation: cake graph compiles (dry run of the Package target);
FindTestAssemblies exercised both ways via a scratch cake script
(real pattern finds 1 assembly, wrong pattern throws); both workflow
files YAML-parse; nuspec and csproj XML-parse; NetStandard solution
builds with 0 warnings / 0 errors.
M21: ClientOptions.Clone() is a hand-maintained public API of the core
(the door factories copy options through it on entry), so it silently
drops the first property someone adds and forgets to copy. Guard against
that rot with a reflection spec that stamps a distinct, non-default value
on every public settable property (including the inherited AuthOptions
members), clones, and asserts the clone matches the original.

Design notes:
- Compares the clone against the ORIGINAL's getter, not the stamped value,
  so a deliberately no-op setter (UseBinaryProtocol discards its value in
  non-MSGPACK builds) compares default-to-default and passes, while a
  forgotten copy still fails (original carries the stamp, clone the default).
- Write-only host fields (RestHost/RealtimeHost/FallbackHosts) are asserted
  through their private backing fields.
- Deep-copied dictionaries (Agents/AuthHeaders/AuthParams/TransportParams)
  are asserted NotBeSameAs + BeEquivalentTo (Clone rebuilds them).
- ChannelDefaults (internal setter) is asserted BeSameAs separately.
- Obsolete members are pragma-618 guarded; the value factory throws on an
  unhandled property type so a new property of a new shape fails here too.
- A separate block covers the internal Logger/NowFunc (by reference) and
  SkipInternetCheck (by value) via InternalsVisibleTo.

Validation: the spec passes; verified non-vacuous by temporarily dropping
the ClientId copy in Clone() and confirming the spec fails, then reverting.
NetStandard solution builds 0 warnings / 0 errors.
M26: the SDK-style door projects Ably.PubSub.Device and
Ably.PubSub.Server carried the legacy project-type GUID
{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} in every solution that lists
them; give them the SDK-style {9A19103F-16F7-4668-BE54-9A1E7A4F7556}.
The net46 heads (Core.NETFramework, Server.NETFramework,
Tests.NETFramework) and IO.Ably.DeltaCodec are genuinely non-SDK and
keep the legacy GUID. Also unify each door's per-project GUID across
solutions (Device -> {ADD94C3B-...}, Server -> {882D22FB-...}) so one
project no longer has three identities; no csproj ProjectReference keys
off these GUIDs, so the change is self-contained to the solutions.

M26 (config): in Ably.PubSub.NetFramework.sln, Ably.PubSub.Server.NETFramework
{7B3C1F42-...} mapped its solution-level  and
configurations to Debug|Any CPU, so a solution-level Package build would
produce and consume Debug bits for the net46 Server asset. Map both
configurations (ActiveCfg + Build.0, all four platforms) to Release|Any CPU,
matching the sibling Ably.PubSub.Core.NETFramework {2D265650-...} precedent in
the same solution. (Ably.PubSub.Package.sln, the solution the packaging
pipeline actually builds, declares no package/package_netstandard solution
configs and already maps Release->Release, so it was correct.)

M10: RootNamespace hints pointed at the assembly names
(Ably.PubSub.Shared, Ably.PubSub.Tests.NETFramework) while the code
namespace is IO.Ably; a contributor adding a file in VS got the wrong
namespace seeded. Set them to IO.Ably and IO.Ably.Tests. Presentation
only - RootNamespace does not affect compiled output here.

M25: the Device csproj comment said '.NET Framework is not a supported
device target', which is wrong - a .NET Framework 4.6.2+ WPF/WinForms app
is a device target and consumes the netstandard2.0 asset, as MAUI does.
Reword to say why there is no separate .NET Framework Device head.

Validation: all four solutions parse (balanced Project/EndProject, BOM
and LF line endings preserved, diff symmetric); FAE04EC0 now appears only
on the net46 heads and DeltaCodec; NetStandard solution builds 0W/0E;
targeted PubSub + Clone specs pass (32/32).
M33: Defaults.GetVersion() read AssemblyFileVersion, which D25 stamps
numeric-only (2.0.0) while the full SemVer including any prerelease
label lives in AssemblyInformationalVersion. A prerelease such as
2.0.0-beta.1 therefore identified on the wire as 2.0.0, indistinguishable
from GA.

Read AssemblyInformationalVersion instead and strip the SourceLink
'+<commit>' build-metadata suffix via a new internal
NormalizeInformationalVersion helper, keeping the SemVer core and the
prerelease label. The old numeric file-version path (first three parts)
stays as a fallback when no informational version is present; the .Take(3)
truncation is deliberately not applied to the informational version, which
would corrupt a prerelease label.

This does NOT include the RTN2g realtime wire-key change (M16), which is
still gated on realtime-team confirmation.

Validation: NetStandard solution builds 0 warnings / 0 errors; new
normalizer theory covers the beta+metadata, GA+metadata, prerelease and
plain cases, plus a spec asserting GetVersion() carries no '+' suffix in
this SourceLink-enabled build; targeted Defaults/Agent/HTTP tests pass
(31/31).
A JSON object returned from AuthCallback is also treated as a
serialized TokenDetails when it has an issued field (TokenDetails.IsToken),
not only when it has a string token field.
Keep realtime retrieval from changing the options of HTTP channels
A realtime channel decoded received messages with a decoding context
built once from the options it was created with, so SetOptions and
Channels.Get(name, options) never changed the cipher used for them
(RTL16, RTS3c). Presence, annotations and publishing already used the
current options.

Update the context's options whenever the channel's options change,
rather than replacing the context, which also holds the delta base
payload that must survive the change (RTL19).
Conflicts resolved:
- AnnotationsRequestParams.cs: keep the RSAN3b querystring sentence,
  take the HttpAnnotations cref and drop the "experimental" wording.
- CHANGELOG.md: take the rewritten 2.0 breaking-change bullets and add
  the FallbackHosts getter note; keep the ExtraParameters bullet next to
  the new Fixed bugs section.
- UPDATING.md: drop "Experimental." from the Annotations row and keep
  the GetAsync parameters sentence.

Follow-on fixes for the merged API:
- AnnotationsTests asserts HttpAnnotations (RestAnnotations was renamed).
- The RTL10b ChannelHistoryTests use the restored public
  HistoryAsync(untilAttach: true); drop the internal-API adaptation and
  its deviations.md entry.
- Un-gate AuthCallbackTests.RSA8d_CallbackReturnsJwt and
  TokenDetailsTests.RSA16b_TokenStringFromCallback, now that an
  AuthCallback may return a token string or JWT; drop their
  deviations.md entry.
- Integration and proxy JWT auth callbacks, and the oversized-token
  RSA4f test, return the string as the spec does, instead of wrapping it
  in TokenDetails.
- Align the uts-to-csharp skill references with HttpAnnotations, the
  public channel untilAttach overloads and string AuthCallback results.
- Move the fatal ERROR + close bullet from the released 1.2.19
  CHANGELOG section to 2.0.0 Behaviour changes.
Move the fatal ERROR followed by socket close entry to Fixed bugs,
since it describes a bug that 2.0 fixes. Drop the
AnnotationsRequestParams limit-exception entry: the annotations API is
new in 2.0, so there is no earlier behaviour to compare with.
Add UTS test infrastructure, realtime and REST UTS-derived tests, and the uts-to-csharp skill
Decode live messages with the channel's current options
Brings in the two main-only changes since the merge base, so the 2.0
line can merge back into main without conflicts.

- RealtimeChannels.cs: keeps the 2.0 RTS4e behaviour (release throws
  90011 unless the channel is Initialized, Detached or Failed). Main's
  RTS4b deprecation warning is the 1.x counterpart and doesn't apply.
- ChannelsSpecs.cs: keeps the 2.0 RTS4c-e tests. Main's RTS4b tests
  merged cleanly but use the 1.x AblyRealtime type and assert the
  deprecated 1.x behaviour, so they are dropped.
- AblySandboxFixture.cs: ports main's race fix (one Lazy provisioning
  task per endpoint in a ConcurrentDictionary, evicted on failure) onto
  the 2.0 endpoint-based fixture, keeping MutableMessagesNamespace.
Test-only changes; no product code.

- ConnectionAwaiter (behind WaitForState) checked the connection state
  before subscribing, so a change landing between the two was missed
  and the wait timed out. It now subscribes first, then checks, removes
  its listener on every exit path, uses TrySetResult, runs continuations
  asynchronously and cancels its timeout delay. New ConnectionAwaiterTests
  cover the already-there, reached, timeout and listener-removal cases;
  the concurrency test disposes the client each of its 200 iterations
  creates.
- LocalDeviceTests registered ProcessingEventCallback after the call
  that raises the second GotPushDeviceDetails on a background task, so
  it could miss it. The callback is now registered first.
- Test classes that read or reset the process-wide LocalDevice.Instance
  (LocalDeviceTests, the ActivationStateMachineTests classes,
  PushChannelTests and PushRealtimeTests) now share one non-parallel
  xUnit collection, and PushRealtimeTests clears LocalDevice.Instance
  after its base teardown, as the others do.
- TaskCompletionAwaiter.Tick decrements atomically, so two concurrent
  ticks can't lose one.
- PresenceSandboxSpecs.Presence_ShouldHaveInternalMapForCurrentConnectionId
  waits on each channel for the specific expected presence event instead
  of the first event of any kind, and names the missing one on timeout.
  An assertion that could never fail is removed.
UtsRecord.ConnectionStates and ConnectionStateChanges only see changes
made after they subscribe. ConnectionStaysFailedWhenTheServerClosesAfterAFatalError
created an AutoConnect client and subscribed afterwards, and its mock
answers the first attempt with a fatal ERROR at once. On a slow runner
the client could reach FAILED, and stay there, before the recorder
subscribed, so the poll for FAILED timed out after 10 seconds. The
failure reproduced locally under CPU load.

Create the client with AutoConnect off, subscribe, then connect. The
test still checks the same race across 20 clients. Two smoke tests in
RealtimeUnitSmokeTests, and MockWebSocketTests' event-order test (which
subscribed after triggering a disconnect whose retry is refused at
once), had the same pattern and could lose the DISCONNECTED or first
CONNECTING change they read, so they now subscribe first too.
Fix races in test helpers that made tests flaky
Remove the old-style Ably.PubSub.Server.NETFramework project and the
lib/net46 asset and dependency group it fed into the Server package. A
.NET Framework app now gets Server's netstandard2.0 asset with Core's
net46 asset, the same pairing Ably.PubSub.Device already uses. The net46
core already grants Ably.PubSub.Server InternalsVisibleTo (keyless and
keyed), and Server compiles the same sources with no platform-specific
code, so nothing else changes.

- Drop the project from the three solutions and the nuspec.
- Package consumption: the ServerApp net462/net48 rows now expect Server
  lib/netstandard2.0 with Core lib/net46; ServerApp also constructs a
  client through the CreateHttpClient(string) overload, offline.
- _Package_Verify_NuGet fails if a door package contains lib/net46 or a
  .NET Framework dependency group; PackagingSpecs asserts both door
  nuspecs ship exactly netstandard2.0, net6.0 and net7.0.
- Update CONTRIBUTING.md, cake-build/README.md and related comments.
Use the netstandard2.0 build of Ably.PubSub.Server on .NET Framework
….Core

The net46 Ably.PubSub.Core.dll is strong-name signed in Package builds,
but it referenced, and shipped next to, the unsigned IO.Ably.DeltaCodec.dll.
A strong-named assembly should reference only strong-named assemblies.

- Add a _Package_Merge_NetFramework Cake task, which _Package_Create_NuGet
  depends on. Like 1.x's _Package_Merge_All, it uses ILRepackHelper to merge
  only IO.Ably.DeltaCodec.dll into the net46 core, internalized and signed
  with Ably.PubSub.snk, and writes the result and the core's XML docs to
  bin/Release/Packaged. Newtonsoft.Json is not merged.
- Pack lib/net46 from Packaged in the core nuspec, and drop the separate
  net46 IO.Ably.DeltaCodec.dll. The netstandard2.0, net6.0 and net7.0
  cores still ship it as a separate file.
- Use the tracked tools/ILRepack.exe name in ILRepackHelper. The lowercase
  name only works on case-insensitive file systems, and every pack now
  runs ILRepack.
- Add a "Newtonsoft.Json on .NET Framework" section to UPDATING.md, and note
  that DeltaCodec isn't shared with ably.io on .NET Framework.
Merge DeltaCodec into the .NET Framework build of Ably.PubSub.Core and document Newtonsoft.Json on .NET Framework
@sacOO7
sacOO7 merged commit eb98c0a into main Nov 2, 2026
51 of 53 checks passed
@sacOO7
sacOO7 deleted the integration/v2 branch November 2, 2026 20:31

This branch was successfully deployed

1 active deployment
staging/pull/1340/features — 7bb29c09 Deployed Nov 2, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

5 participants