feat(bpmn): adopt Bpmn.* 0.2.0, and start shipping the two BPMN modules (#7970)

Bpmn.Interchange, Bpmn.Model and Bpmn.Semantics move from 0.1.1-preview.19 to
0.2.0. The three changes below are one unit: the bump is what makes the other
two true.

Retire the private feed. All three packages are on nuget.org at 0.2.0 --
including Bpmn.Semantics, which had no stable release under 0.1.x. The
bpmn-feedz source and its Bpmn.* packageSourceMapping entry both go, which
removes a setup step for every consumer. This is not merely cleanup: the feedz
feed does not carry 0.2.0 stable, so with the mapping left in place the bump
would not restore at all. Restore now resolves Bpmn.* from nuget.org via the
existing `*` mapping.

Lift IsPackable=false. The comment on the flag named its own removal condition
-- the Bpmn.* packages reaching nuget.org -- and that condition is now met, so
Elsa.Bpmn and Elsa.Bpmn.Interchange begin shipping. Both pack with every
dependency publicly restorable, and both emit a package manifest carrying
runtimeKinds ["elsa.server"], so neither is silently excluded from the catalog.
They were the last two IsPackable=false projects under src/.

Flip the compensation pin to the fixed behaviour. 0.2.0 contains the fix for
valence-works/bpmn#13 (filed from here as #7959), so
CompensationRunCancelledMidReplay went red on the bump exactly as it was built
to. Upstream took the wide fix: every token a cancelled transaction abandons now
gets a real teardown. The head handler still starts twice -- that is the release
being real -- but the first run is now torn down, so the scope is left holding
one live record for the slot instead of two. The applier is deliberately
unpatched; the assertions moved to describe the fix, not to accommodate it.

Note on persisted state: 0.2.0 freezes the payload format at 1.0.0 and state
persisted by 0.1.x no longer deserializes. Elsa persists the library's
BpmnExecutionState into workflow state, so an in-flight BPMN instance does not
survive this bump. Neither module has ever been published, so no released
consumer can be holding such state -- which is why this is the moment to take
the break.

BpmnRuntimeCapabilitiesTests stays green: 0.2.0 defines no capability flag Elsa
does not already declare. The new cancel-end-event requirement is
SubtreeCancellation, which BpmnRuntimeCapabilities.Declared already carries.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Sipke Schoorstra 2026-08-21 01:44:07 +02:00 committed by GitHub
parent 1b38c3511d
commit b24beadca9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 26 additions and 23 deletions

View file

@ -98,9 +98,9 @@
<PackageVersion Include="Azure.ResourceManager.ServiceBus" Version="1.1.0"/>
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8"/>
<PackageVersion Include="Bogus" Version="35.6.5"/>
<PackageVersion Include="Bpmn.Interchange" Version="0.1.1-preview.19"/>
<PackageVersion Include="Bpmn.Model" Version="0.1.1-preview.19"/>
<PackageVersion Include="Bpmn.Semantics" Version="0.1.1-preview.19"/>
<PackageVersion Include="Bpmn.Interchange" Version="0.2.0"/>
<PackageVersion Include="Bpmn.Model" Version="0.2.0"/>
<PackageVersion Include="Bpmn.Semantics" Version="0.2.0"/>
<PackageVersion Include="ConfigureAwait.Fody" Version="3.3.2" PrivateAssets="All"/>
<PackageVersion Include="ConsoleLogStreaming.Contracts" Version="1.1.0"/>
<PackageVersion Include="ConsoleLogStreaming.Core" Version="1.1.0"/>

View file

@ -6,7 +6,6 @@
<add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
<add key="cshells-feedz" value="https://f.feedz.io/sfmskywalker/cshells/nuget/index.json" />
<add key="elsa-preview-feedz" value="https://f.feedz.io/elsa-workflows/elsa-3/nuget/index.json" />
<add key="bpmn-feedz" value="https://f.feedz.io/valence-works/bpmn/nuget/index.json" />
<add key="valence-loom-feedz" value="https://f.feedz.io/valence-works/loom/nuget/index.json" />
</packageSources>
<packageSourceMapping>
@ -23,9 +22,6 @@
<package pattern="Elsa.Platform.PackageManifest.Generator" />
<package pattern="Elsa.Platform.PackageManifest" />
</packageSource>
<packageSource key="bpmn-feedz">
<package pattern="Bpmn.*" />
</packageSource>
<packageSource key="valence-loom-feedz">
<package pattern="Loom" />
<package pattern="Loom.*" />

View file

@ -5,8 +5,6 @@
Provides BPMN XML interchange support by wiring the Bpmn.Interchange library into Elsa.
</Description>
<PackageTags>elsa extension module bpmn interchange</PackageTags>
<!-- Not packable until the Bpmn.* packages are published to nuget.org; they currently live only on the valence-works/bpmn feedz feed, so a published Elsa.Bpmn.Interchange would fail to restore for consumers. -->
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>

View file

@ -5,8 +5,6 @@
Provides BPMN execution support by wiring the Bpmn.Model and Bpmn.Semantics libraries into Elsa.
</Description>
<PackageTags>elsa extension module bpmn</PackageTags>
<!-- Not packable until the Bpmn.* packages are published to nuget.org; they currently live only on the valence-works/bpmn feedz feed, so a published Elsa.Bpmn would fail to restore for consumers. -->
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>

View file

@ -88,23 +88,34 @@ public class BpmnCompensationTests(ITestOutputHelper testOutputHelper)
// Act: the other branch cancels the transaction, which stops the replay's coordinating token.
await _host.FinishWorkAsync("fraudCheck");
// Assert: the released entries are registered again, so the cancellation's own replay claims them -- the head
// handler starting a second time is both the first half of the release being real *and* the symptom of
// valence-works/bpmn#13: BpmnInterpreter.CancelTransaction drops the live work it is abandoning without ever
// producing a PendingTeardown/CancelWorkSubtree command for it, so the host's original ledger record and
// bookmark for the releaseSeat slot survive untouched alongside the replay's freshly started one. The scope
// now holds two live records for the same (BindingRef, IterationId) slot -- pinned below so this fails the
// moment the library fix lands, at which point both counts become 1. Filed from here as #7959, closed as
// routed upstream; the applier is deliberately unpatched so this stays visible rather than worked around.
// Assert: the released entries are registered again, so the cancellation's own replay claims them, and the head
// handler starts a second time -- the first half of the release being real.
//
// The flip may not be mechanical. Upstream is choosing between tearing down only the compensation handler it
// re-starts and tearing down every abandoned token; the second also cancels transaction branches still in
// flight, which can move other scenarios in this suite. Check which shipped when the Bpmn.Semantics pin moves.
// The second start is a *replacement*, not a duplicate, and that is what valence-works/bpmn#13 fixed in
// Bpmn.Semantics 0.2.0. CancelTransaction used to abandon the live work it was superseding without issuing a
// CancelWorkSubtree for it, so the host's original ledger record and bookmark for the releaseSeat slot
// survived alongside the replay's freshly started one -- two live records for one (BindingRef, IterationId)
// slot, which BpmnHostSnapshot documents as a host obligation never to allow, and which the interpreter's own
// BuildLiveWorkHandles then collapses last-wins. Upstream took the wide fix: every token a cancelled
// transaction abandons now gets a real teardown, not just the compensation handler being re-started.
//
// So all three of these are the fix, and each fails differently if it regresses: the handler runs twice
// (release), the first run is torn down (teardown), and the slot is left holding exactly one record
// (the invariant).
Assert.Equal(2, _host.Log.Occurrences("executed:releaseSeat"));
Assert.Contains("cancelled:releaseSeat", _host.Log.Entries);
var releaseSeatBindingRef = BpmnTestProcesses.BindingRef("releaseSeat");
var releaseSeatLiveRecords = _host.LiveWorkOf("sub").Count(record => record.BindingRef == releaseSeatBindingRef);
Assert.Equal(2, releaseSeatLiveRecords);
Assert.Equal(1, releaseSeatLiveRecords);
// And the invariant holds at the moment that matters, not just once the dust settles. 0.2.0 states the
// at-most-one-live-unit-per-slot rule as holding after a command batch is applied in full and in order,
// because an interrupting path may emit the replacement StartWork ahead of the CancelWorkSubtree it
// supersedes. This snapshot is taken inside the replacement's own execution -- overwritten on each start, so
// it is the second one -- and a host that applied the batch out of order, or keyed its ledger by slot rather
// than by handle, would be holding two records here even though the count above settles at one.
Assert.Equal([releaseSeatBindingRef], _host.Log.Snapshot("liveWork@releaseSeat"));
// And the entry that was claimed but never ran is reached once that head handler finishes: the other half.
var result = await _host.FinishWorkAsync("releaseSeat");