From b24beadca989953ff3ff55ec3695a64cb1936e9f Mon Sep 17 00:00:00 2001 From: Sipke Schoorstra Date: Fri, 21 Aug 2026 01:44:07 +0200 Subject: [PATCH] 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 --- Directory.Packages.props | 6 ++-- NuGet.Config | 4 --- .../Elsa.Bpmn.Interchange.csproj | 2 -- src/modules/Elsa.Bpmn/Elsa.Bpmn.csproj | 2 -- .../HostPort/BpmnCompensationTests.cs | 35 ++++++++++++------- 5 files changed, 26 insertions(+), 23 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 6f4c89e3b..24137c54f 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -98,9 +98,9 @@ - - - + + + diff --git a/NuGet.Config b/NuGet.Config index ec08ff52f..a154be3de 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -6,7 +6,6 @@ - @@ -23,9 +22,6 @@ - - - diff --git a/src/modules/Elsa.Bpmn.Interchange/Elsa.Bpmn.Interchange.csproj b/src/modules/Elsa.Bpmn.Interchange/Elsa.Bpmn.Interchange.csproj index c5e681073..30716a633 100644 --- a/src/modules/Elsa.Bpmn.Interchange/Elsa.Bpmn.Interchange.csproj +++ b/src/modules/Elsa.Bpmn.Interchange/Elsa.Bpmn.Interchange.csproj @@ -5,8 +5,6 @@ Provides BPMN XML interchange support by wiring the Bpmn.Interchange library into Elsa. elsa extension module bpmn interchange - - false diff --git a/src/modules/Elsa.Bpmn/Elsa.Bpmn.csproj b/src/modules/Elsa.Bpmn/Elsa.Bpmn.csproj index 79d5dcd3a..d2d8c22f7 100644 --- a/src/modules/Elsa.Bpmn/Elsa.Bpmn.csproj +++ b/src/modules/Elsa.Bpmn/Elsa.Bpmn.csproj @@ -5,8 +5,6 @@ Provides BPMN execution support by wiring the Bpmn.Model and Bpmn.Semantics libraries into Elsa. elsa extension module bpmn - - false diff --git a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnCompensationTests.cs b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnCompensationTests.cs index 5945eca35..0f631adba 100644 --- a/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnCompensationTests.cs +++ b/test/integration/Elsa.Bpmn.IntegrationTests/Scenarios/HostPort/BpmnCompensationTests.cs @@ -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");