From ad6cd9d90147055f14386c822e042bf6f6aa21a9 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 14 May 2026 14:17:06 -0400 Subject: [PATCH 1/4] fix(dispatcher): honor caller-supplied :causation_id on persisted events The execution context dropped pipeline.causation_id and used command_uuid, breaking chain-of-causation reconstruction from events alone for any multi-event flow where a process manager or event handler dispatches follow-up commands. Signed-off-by: Yordis Prieto --- lib/commanded/commands/dispatcher.ex | 9 ++++++-- test/commands/correlation_causation_test.exs | 22 ++++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/lib/commanded/commands/dispatcher.ex b/lib/commanded/commands/dispatcher.ex index 034e6ac7..c8133312 100644 --- a/lib/commanded/commands/dispatcher.ex +++ b/lib/commanded/commands/dispatcher.ex @@ -156,7 +156,12 @@ defmodule Commanded.Commands.Dispatcher do end defp to_execution_context(%Pipeline{} = pipeline, %Payload{} = payload) do - %Pipeline{command: command, command_uuid: command_uuid, metadata: metadata} = pipeline + %Pipeline{ + command: command, + command_uuid: command_uuid, + causation_id: causation_id, + metadata: metadata + } = pipeline %Payload{ correlation_id: correlation_id, @@ -170,7 +175,7 @@ defmodule Commanded.Commands.Dispatcher do %ExecutionContext{ command: command, - causation_id: command_uuid, + causation_id: causation_id || command_uuid, correlation_id: correlation_id, metadata: metadata, handler: handler_module, diff --git a/test/commands/correlation_causation_test.exs b/test/commands/correlation_causation_test.exs index 95055db2..f753f1da 100644 --- a/test/commands/correlation_causation_test.exs +++ b/test/commands/correlation_causation_test.exs @@ -38,7 +38,7 @@ defmodule Commanded.Commands.CorrelationCasuationTest do assert [^causation_id] = CommandAuditMiddleware.dispatched_commands(& &1.causation_id) end - test "should be set from `command_uuid` on created event" do + test "should fall back to `command_uuid` on created event when not provided" do command = %OpenAccount{account_number: "ACC123", initial_balance: 500} :ok = BankRouter.dispatch(command, application: BankApp) @@ -46,9 +46,19 @@ defmodule Commanded.Commands.CorrelationCasuationTest do [command_uuid] = CommandAuditMiddleware.dispatched_commands(& &1.command_uuid) [event] = EventStore.stream_forward(BankApp, "ACC123") |> Enum.to_list() - # an event's `causation_id` is the dispatched command's `command_uuid` assert event.causation_id == command_uuid end + + test "should be set from explicit `:causation_id` dispatch option on created event" do + causation_id = UUID.uuid4() + command = %OpenAccount{account_number: "ACC123", initial_balance: 500} + + :ok = BankRouter.dispatch(command, application: BankApp, causation_id: causation_id) + + [event] = EventStore.stream_forward(BankApp, "ACC123") |> Enum.to_list() + + assert event.causation_id == causation_id + end end describe "`correlation_id`" do @@ -131,17 +141,11 @@ defmodule Commanded.Commands.CorrelationCasuationTest do [account_opened, money_deposited] = EventStore.stream_forward(BankApp, "ACC123") |> Enum.to_list() - # should set command causation id as handled event id [_causation_id, causation_id] = CommandAuditMiddleware.dispatched_commands(& &1.causation_id) assert causation_id == account_opened.event_id - - # should set created event causation id as the command uuid - [_command_uuid, command_uuid] = - CommandAuditMiddleware.dispatched_commands(& &1.command_uuid) - - assert money_deposited.causation_id == command_uuid + assert money_deposited.causation_id == account_opened.event_id end def start_account_bonus_handler(_context) do From f50ad7c5e53e1c3e3c72b901d8e4a4fef5431f28 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 14 May 2026 14:23:03 -0400 Subject: [PATCH 2/4] chore(ci): comply with org action allowlist and SHA-pin policy The org policy blocks the conventional-commits action and any tag-pinned reference, so CI has been failing at startup on every PR since the policy landed. Signed-off-by: Yordis Prieto --- .github/workflows/ci.yml | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f848d60f..af7a0a68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,10 +20,14 @@ jobs: name: Conventional Commits runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4.1.3 - - uses: webiny/action-conventional-commits@v1.3.0 - with: - allowed-commit-types: "feat,fix,chore" + - name: Validate PR title + env: + PR_TITLE: ${{ github.event.pull_request.title }} + run: | + if ! [[ "$PR_TITLE" =~ ^(feat|fix|chore)(\([^)]+\))?!?:\ .+ ]]; then + echo "PR title must follow Conventional Commits (feat|fix|chore): $PR_TITLE" + exit 1 + fi quality-assurance: name: Quality Assurance @@ -48,17 +52,17 @@ jobs: otp: 27 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Set up Elixir id: beam - uses: erlef/setup-beam@v1 + uses: erlef/setup-beam@fc68ffb90438ef2936bbb3251622353b3dcb2f93 # v1.24.0 with: elixir-version: ${{ matrix.elixir }} otp-version: ${{ matrix.otp }} - name: Restore dependencies cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 with: path: deps key: ${{ runner.os }}-${{ steps.beam.outputs.otp-version }}-${{ steps.beam.outputs.elixir-version }}-mix-${{ hashFiles('**/mix.lock') }} @@ -87,7 +91,7 @@ jobs: run: mix credo --strict - name: Retrieve Dialyzer PLT cache - uses: actions/cache@v4 + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 id: plt-cache with: path: priv/plts From 45a3f950747f3de053d9c2c656dafaaa63eb654d Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 14 May 2026 14:31:03 -0400 Subject: [PATCH 3/4] chore(ci): restore conventional-commits action now that it is allowlisted Signed-off-by: Yordis Prieto --- .github/workflows/ci.yml | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index af7a0a68..e1a256ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,14 +20,10 @@ jobs: name: Conventional Commits runs-on: ubuntu-latest steps: - - name: Validate PR title - env: - PR_TITLE: ${{ github.event.pull_request.title }} - run: | - if ! [[ "$PR_TITLE" =~ ^(feat|fix|chore)(\([^)]+\))?!?:\ .+ ]]; then - echo "PR title must follow Conventional Commits (feat|fix|chore): $PR_TITLE" - exit 1 - fi + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - uses: webiny/action-conventional-commits@8bc41ff4e7d423d56fa4905f6ff79209a78776c7 # v1.3.0 + with: + allowed-commit-types: "feat,fix,chore" quality-assurance: name: Quality Assurance From efb195f6d1dd2a5ea580651d17161a22dee8e885 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Thu, 14 May 2026 14:50:33 -0400 Subject: [PATCH 4/4] chore(format): apply mix format Pre-existing drift surfaced by the first CI run since the action allowlist was unblocked; unrelated to the dispatcher fix on this branch. Signed-off-by: Yordis Prieto --- lib/commanded/opentelemetry/aggregate.ex | 3 ++- lib/commanded/opentelemetry/event_handler.ex | 6 ++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/commanded/opentelemetry/aggregate.ex b/lib/commanded/opentelemetry/aggregate.ex index 882563f1..95fca5a7 100644 --- a/lib/commanded/opentelemetry/aggregate.ex +++ b/lib/commanded/opentelemetry/aggregate.ex @@ -47,7 +47,8 @@ defmodule Commanded.OpenTelemetry.Aggregate do {MessagingAttributes.messaging_destination_name(), handler_module_name}, {MessagingAttributes.messaging_message_id(), context.causation_id}, {MessagingAttributes.messaging_message_conversation_id(), context.correlation_id}, - {MessagingAttributes.messaging_consumer_group_name(), Helpers.module_name(meta.application)}, + {MessagingAttributes.messaging_consumer_group_name(), + Helpers.module_name(meta.application)}, # OTel Code SemConv {CodeAttributes.code_function(), to_string(context.function)}, {CodeAttributes.code_namespace(), handler_module_name}, diff --git a/lib/commanded/opentelemetry/event_handler.ex b/lib/commanded/opentelemetry/event_handler.ex index d1ddef53..7e97e3f9 100644 --- a/lib/commanded/opentelemetry/event_handler.ex +++ b/lib/commanded/opentelemetry/event_handler.ex @@ -81,7 +81,8 @@ defmodule Commanded.OpenTelemetry.EventHandler do {MessagingAttributes.messaging_destination_subscription_name(), meta.handler_name}, {MessagingAttributes.messaging_message_id(), recorded_event.event_id}, {MessagingAttributes.messaging_message_conversation_id(), recorded_event.correlation_id}, - {MessagingAttributes.messaging_consumer_group_name(), Helpers.module_name(meta.application)}, + {MessagingAttributes.messaging_consumer_group_name(), + Helpers.module_name(meta.application)}, # OTel Code SemConv {CodeAttributes.code_function(), "handle"}, {CodeAttributes.code_namespace(), handler_module_name}, @@ -184,7 +185,8 @@ defmodule Commanded.OpenTelemetry.EventHandler do {MessagingAttributes.messaging_operation_name(), "batch"}, {MessagingAttributes.messaging_destination_name(), handler_module_name}, {MessagingAttributes.messaging_destination_subscription_name(), meta.handler_name}, - {MessagingAttributes.messaging_consumer_group_name(), Helpers.module_name(meta.application)}, + {MessagingAttributes.messaging_consumer_group_name(), + Helpers.module_name(meta.application)}, {MessagingAttributes.messaging_batch_message_count(), meta.event_count}, # OTel Code SemConv {CodeAttributes.code_function(), "handle_batch"},