Skip to content

Wolverine 5→6 upgrade + transactional-outbox atomicity fix#154

Merged
emeraldleaf merged 4 commits into
mainfrom
chore/wolverine-6-upgrade
Jun 15, 2026
Merged

Wolverine 5→6 upgrade + transactional-outbox atomicity fix#154
emeraldleaf merged 4 commits into
mainfrom
chore/wolverine-6-upgrade

Conversation

@emeraldleaf

@emeraldleaf emeraldleaf commented Jun 14, 2026

Copy link
Copy Markdown
Owner

Major-version bump of all six WolverineFx.* packages → 6.8.0, plus the fix for a production correctness regression the upgrade introduced. Build is source-compatible; the runtime breaking changes are caught by the integration suite.

📖 Full war story (investigation, wrong turns, root cause, lessons — written for the portfolio): docs/war-story-wolverine6-outbox-atomicity.md

The three breaking changes

  1. Runtime code generator split out of core (GH-2876) → reference WolverineFx.RuntimeCompilation from ServiceDefaults.
  2. ServiceLocationPolicy default flipped to NotAllowedAllowHandlerServiceLocation() shared extension in all 5 services. (A codegen knob, not the service-locator anti-pattern.)
  3. Constructor-injected IMessageBus is no longer enlisted in the handler's outbox transaction. This is the serious one — it silently broke the transactional outbox.

The outbox regression (the heart of this PR)

Handlers published events via IEventPublisher (a shim over constructor-injected IMessageBus). In 5.x that publish was enlisted in the handler transaction (staged → atomic). In 6.x it fires inline, before SaveChanges commits. Proven with a rollback test: force the commit to fail after publishing, and OrderPlacedEvent was still dispatched despite the order rolling back.

Production impact (transport-agnostic — real ASB, emulator, RabbitMQ alike): PlaceOrder publishes → event hits the broker → SaveChanges fails → the order never persists, but PaymentService already consumed the event and charges for an order that doesn't exist. Intermittent, invisible to happy-path tests, money-touching. This is exactly what the outbox exists to prevent — so the upgrade must not merge without this fix.

Fix: publish through the IMessageContext Wolverine injects as a HandleAsync method parameter (the only enlisted context), in every write-then-publish handler — PlaceOrder, CreateShipment, PaymentService Gateway, and PaymentService's local Acceptor→Gateway continuation. One parameter each; the four heavier hypotheses we tried first (UseDurableLocalQueues, constructor-IMessageContext, both) were all wrong and reverted.

Dead-code removal: the IEventPublisher shim, now unused by OrderService/ShippingService handlers, was deleted (port + impl + registration). Retained only in PaymentService for two non-write-then-publish paths (Acceptor terminal-event republish; PaymentRecoveryJob).

New test

OrderSagaTests.PlaceOrder_does_not_dispatch_OrderPlacedEvent_when_the_commit_rolls_back + ThrowingSaveChangesInterceptor — RED before the fix, GREEN after. The first assertion (no order persisted) guards that the interceptor actually fired, so the atomicity assertion can't give a false pass.

Encoding

  • CLAUDE.md: new rule — in-handler transactional publishing needs a method-injected IMessageContext, not IEventPublisher.
  • docs/project-decisions.md §13: full upgrade notes; the cross-service caveat updated from "unverified" → "proven and fixed".
  • docs/war-story-...md: the narrative.

/check-rules audit: CLAUDE.md changed; paired inline comments end with the canonical pointer; detail lives in docs/project-decisions.md per the lean-CLAUDE.md rule.

Outbox atomicity — now complete across handler and non-handler code

PaymentRecoveryJob (the non-handler recovery sweeper) had the same latent non-enlistment. Fixed with Wolverine's non-handler outbox API (IDbContextOutboxEnroll + SaveChangesAndFlushMessagesAsync), proven by a new "outbox-in-non-handler" rollback test (PaymentRecoveryAtomicityTests). Closes #153.

Tests

  • dotnet build — clean, 0 warnings (TreatWarningsAsErrors on).
  • 116 unit/arch + 40 integration green (Catalog 19, Order 11, Payment 6, Shipping 4).

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a Codacy CLI bootstrap script with OS/architecture-aware downloads and per-user version caching.
  • Bug Fixes

    • Improved transactional outbox consistency by staging message publishes via Wolverine message context, preventing events from dispatching when commits roll back (order, shipping, and payment recovery flows).
  • Documentation

    • Expanded Wolverine 5→6 upgrade notes and updated upgrade status/war story with outbox atomicity guidance.
  • Chores

    • Upgraded WolverineFx to 6.8.0, enabled handler service-location, and pinned runtime/tool versions.
  • Tests

    • Added integration coverage to verify no event dispatch on rollback for both handler and non-handler recovery.

… outbox enlistment)

Major-version bump of all six WolverineFx.* packages. Build is source-compatible;
three runtime breaking changes surfaced and were fixed (all caught by the integration suite):

1. Runtime code generator split out of core (GH-2876) → reference WolverineFx.RuntimeCompilation
   from ServiceDefaults (flows to every service). Restores TypeLoadMode.Dynamic behavior.

2. ServiceLocationPolicy default flipped to NotAllowed (rejects codegen container-fallback for
   non-inlinable deps like IProductCache/HybridCache) → AllowHandlerServiceLocation() shared
   extension, called in all five services. Not the service-locator anti-pattern; a codegen knob.

3. Constructor-injected IMessageBus (the IEventPublisher shim) is no longer enlisted in the
   handler's outbox transaction under 6.x — a publish through it fires immediately, before commit.
   This broke PaymentService's Acceptor→Gateway split: the local PaymentProcessingRequested
   continuation reached the Gateway handler before Payment(Pending) committed, so the Gateway
   found no row and the payment stuck in Pending. Fix: publish the continuation via a
   method-injected IMessageContext (enlisted) in ProcessPaymentHandler.

Encoding: new always-on rule in CLAUDE.md; full upgrade notes in docs/project-decisions.md;
corrected the now-false "stages in the same transaction" claim in all three WolverineEventPublisher
XML docs. STATUS.md updated.

Open follow-up (tracked in Issues): verify outbox atomicity of OrderService/ShippingService
external publishes (same constructor-IMessageBus non-enlistment) and apply IMessageContext if violated.

Tests: build clean (0 warnings, TreatWarningsAsErrors on); 116 unit/arch + 38/38 integration green.

/check-rules audit: CLAUDE.md changed (added the IMessageContext outbox-enlistment rule); the
paired inline comments (ProcessPayment.cs, the 3 WolverineEventPublisher docs) end with the
canonical pointer and the detail lives in docs/project-decisions.md per the lean-CLAUDE.md rule.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 14, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@emeraldleaf, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 39 minutes and 49 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 97482108-785d-4bec-bbe8-d2a3b8826e9d

📥 Commits

Reviewing files that changed from the base of the PR and between 7ef1f0c and fc42730.

📒 Files selected for processing (7)
  • .gitignore
  • CLAUDE.md
  • docs/code-flows/orderservice.md
  • docs/code-flows/shippingservice.md
  • docs/observability.md
  • tests/OrderService.Tests.Integration/OrderSagaTests.cs
  • tests/OrderService.Tests.Integration/ThrowingSaveChangesInterceptor.cs

Walkthrough

Upgrades WolverineFx from 5.39.3 to 6.8.0 across all services, addressing three runtime-breaking changes: adds WolverineFx.RuntimeCompilation, introduces an AllowHandlerServiceLocation extension applied in every service host, and refactors write-then-publish handlers in OrderService, PaymentService, and ShippingService to use method-injected IMessageContext for transactional outbox atomicity. Additionally refactors non-handler PaymentRecoveryJob to use IDbContextOutbox for atomic payment failure recovery. Removes unused IEventPublisher shims, adds integration tests verifying outbox atomicity under rollback, includes comprehensive war-story documentation, and adds Codacy CLI bootstrap tooling.

Changes

WolverineFx 5→6 Upgrade and Multi-Service Outbox Refactoring

Layer / File(s) Summary
NuGet version bump and RuntimeCompilation restore
Directory.Packages.props, NextAurora.ServiceDefaults/NextAurora.ServiceDefaults.csproj
All WolverineFx packages bumped from 5.39.3 to 6.8.0; WolverineFx.RuntimeCompilation added to ServiceDefaults to restore dynamic handler code generation.
AllowHandlerServiceLocation extension and wiring
NextAurora.ServiceDefaults/Extensions.cs, CatalogService/Program.cs, NotificationService/Program.cs, OrderService/Program.cs, PaymentService/Program.cs, ShippingService/Program.cs
New AllowHandlerServiceLocation extension sets ServiceLocationPolicy to AlwaysAllowed; applied in all five service hosts' UseWolverine blocks to restore the Wolverine 5 default behavior under the new policy-restricted default.
OrderService PlaceOrderHandler switched to IMessageContext
OrderService/Features/PlaceOrder.cs
HandleAsync gains IMessageContext method parameter; constructor dependency on IEventPublisher removed; OrderPlacedEvent publishing switches from eventPublisher.PublishAsync to messageContext.PublishAsync with explicit cancellation check before staging, ensuring outbox enlistment with the entity write.
PaymentService handlers switched to IMessageContext
PaymentService/Features/ProcessPayment.cs
Both ProcessPaymentHandler and PaymentProcessingRequestedHandler gain IMessageContext method parameters; continuation and completion event publishing (PaymentProcessingRequested, PaymentCompletedEvent, PaymentFailedEvent) shifts from constructor-injected IEventPublisher to messageContext.PublishAsync; PaymentProcessingRequestedHandler drops unused IEventPublisher constructor dependency.
ShippingService CreateShipmentHandler switched to IMessageContext
ShippingService/Features/CreateShipment.cs
HandleAsync gains IMessageContext method parameter; constructor dependency on IEventPublisher removed; ShipmentDispatchedEvent publishing switches from eventPublisher.PublishAsync to messageContext.PublishAsync, staging the event within the EF Core transaction.
PaymentService PaymentRecoveryJob switched to IDbContextOutbox
PaymentService/Infrastructure/PaymentRecoveryJob.cs, PaymentService/PaymentService.csproj
Non-handler recovery sweep refactored from constructor-injected IEventPublisher to Wolverine's IDbContextOutbox API; per-row recovery now uses outbox.Enroll(context), outbox.PublishAsync(...), and outbox.SaveChangesAndFlushMessagesAsync(ct) for atomic staging of PaymentFailedEvent alongside payment state updates; InternalsVisibleTo expanded to include integration test assembly.
Dependency injection cleanup: remove IEventPublisher shims
OrderService/Infrastructure/DependencyInjection.cs, OrderService/Domain/IEventPublisher.cs, ShippingService/Infrastructure/DependencyInjection.cs, ShippingService/Domain/IEventPublisher.cs
IEventPublisher-to-WolverineEventPublisher DI registrations removed from OrderService and ShippingService; IEventPublisher interface files deleted; composition-root comments updated to reflect method-injected IMessageContext publishing pattern.
WolverineEventPublisher documentation updated for Wolverine 6 enlistment semantics
OrderService/Infrastructure/WolverineEventPublisher.cs, PaymentService/Infrastructure/WolverineEventPublisher.cs, ShippingService/Infrastructure/WolverineEventPublisher.cs
XML documentation clarifies that constructor-injected IMessageBus dispatches immediately without outbox enlistment; directs callers to use method-injected IMessageContext for transactional outbox-staged publishing.
Outbox atomicity verification under rollback for handlers and non-handlers
tests/OrderService.Tests.Integration/ThrowingSaveChangesInterceptor.cs, tests/OrderService.Tests.Integration/OrderSagaTests.cs, tests/PaymentService.Tests.Integration/ThrowOnPaymentUpdateInterceptor.cs, tests/PaymentService.Tests.Integration/PaymentRecoveryAtomicityTests.cs
New ThrowingSaveChangesInterceptor EF Core interceptor simulates commit failure after event publish; new PlaceOrder_does_not_dispatch_OrderPlacedEvent_when_the_commit_rolls_back integration test verifies OrderPlacedEvent atomicity; new ThrowOnPaymentUpdateInterceptor and RecoverySweep_does_not_dispatch_PaymentFailedEvent_when_the_commit_rolls_back test verifies non-handler outbox atomicity under rollback for PaymentRecoveryJob.
Comprehensive upgrade war-story and root cause analysis
docs/war-story-wolverine6-outbox-atomicity.md
New war-story document details the Wolverine 5→6 outbox atomicity regression, chronicles the investigation (false leads and breakthroughs via SQL ordering observation), explains the root cause (constructor-injected IMessageBus vs. method-injected IMessageContext enlistment distinction), shows rollback validation test proving external event non-atomicity, specifies the fix mechanism across handlers and PaymentRecoveryJob, documents deliberate non-changes for terminal-event republish path, and includes lessons and appendix on related breaking changes.
Upgrade notes, CLAUDE rule, and status tracking
CLAUDE.md, docs/project-decisions.md, docs/STATUS.md
New project-decisions section enumerates three runtime-breaking changes and cross-service outbox-atomicity caveat with applied fixes across handlers and PaymentRecoveryJob; CLAUDE.md adds rule requiring IMessageContext for in-transaction handler publishing; STATUS.md updated with in-flight version range and open follow-up on external publish atomicity verification.

Codacy CI Tooling Setup

Layer / File(s) Summary
Codacy CLI bootstrap script with version caching and GitHub release detection
.codacy/cli.sh
New script handles OS/arch detection, version.yaml caching, GitHub release API fetching with rate-limit handling and optional GH_TOKEN authentication, tarball download via curl/wget, extraction to version-specific directory, binary marking as executable, and dispatch to downloaded binary or download-only success mode.
Codacy runtime and tool version pins
.codacy/codacy.yaml
Pins runtime versions for Dart, Go, Java, Node, and Python and tool versions for dartanalyzer, eslint, lizard, opengrep, pmd, pylint, revive, and trivy.

Sequence Diagram

sequenceDiagram
  participant Handler as OrderService/PaymentService<br/>PlaceOrderHandler
  participant MessageContext as IMessageContext<br/>(method param)
  participant DbContext as EF Core DbContext
  participant Outbox as wolverine_outgoing<br/>_envelopes
  participant Dispatcher as Event Dispatcher<br/>(post-commit)
  
  Handler->>MessageContext: PublishAsync(Event)
  MessageContext->>Outbox: Stage envelope in transaction
  Handler->>DbContext: SaveChangesAsync()
  DbContext->>Outbox: Commit staged envelope atomically
  DbContext-->>Handler: Transaction complete
  Dispatcher->>Outbox: Poll and dispatch
  Dispatcher->>Dispatcher: Send Event downstream
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related issues

  • [epic/blocked] Wolverine 5.x → 6.x migration #72: This PR directly implements the WolverineFx 5.x → 6.x migration epic tracked in that issue, including the package bump to 6.8.0, WolverineFx.RuntimeCompilation addition, AllowHandlerServiceLocation() extension across all services, refactoring write-then-publish handlers to use IMessageContext method parameters for transactional outbox behavior in OrderService, PaymentService, and ShippingService, refactoring non-handler PaymentRecoveryJob to use IDbContextOutbox, removing unused IEventPublisher shims, and adding integration tests to verify outbox atomicity under rollback.

  • PaymentRecoveryJob: outbox atomicity for non-handler publishing under Wolverine 6 #153: This PR addresses the outbox atomicity regression discovered in that issue by refactoring all write-then-publish handlers (OrderService, PaymentService, ShippingService) to use method-injected IMessageContext for transactional event staging, and refactoring the non-handler PaymentRecoveryJob to use IDbContextOutbox for atomic recovery sweeps, with corresponding integration test coverage for rollback scenarios.

Possibly related PRs

  • emeraldleaf/NextAurora#40: Both PRs modify PaymentService/Features/ProcessPayment.cs handlers, with the main PR further refactoring them to publish via IMessageContext method injection for outbox atomicity, building directly on the acceptor/gateway handler split introduced in that PR.

  • emeraldleaf/NextAurora#61: Both PRs upgrade WolverineFx-related PackageVersion entries in Directory.Packages.props, establishing the same centrally managed NuGet version strategy.

  • emeraldleaf/NextAurora#30: Both PRs overlap substantially in handler and outbox code paths across OrderService, PaymentService, and ShippingService; the main PR shifts publishing to method-injected IMessageContext/IDbContextOutbox for atomicity while the retrieved PR addresses repository wrapper refactoring, creating related but distinct development checkpoints in the same services.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the main objectives: upgrading Wolverine from 5→6 and fixing transactional-outbox atomicity issues that arose from the upgrade.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch chore/wolverine-6-upgrade

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 14, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
NotificationService/Program.cs 0.00% 1 Missing ⚠️
PaymentService/Features/ProcessPayment.cs 88.88% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 10

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
PaymentService/Features/ProcessPayment.cs (1)

234-271: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Publish the terminal events through the enlisted context too.

PaymentCompletedEvent / PaymentFailedEvent still go out via the constructor-injected publisher before SaveChangesAsync, but this PR documents that constructor-injected Wolverine publishes are no longer transaction-enlisted in 6.x. If SaveChangesAsync fails, consumers can observe a terminal payment event for a row that never committed.

♻️ Suggested fix
 public class PaymentProcessingRequestedHandler(
     PaymentDbContext context,
     IPaymentGateway gateway,
-    IEventPublisher eventPublisher)
+    )
 {
-    public async Task HandleAsync(PaymentProcessingRequested message, CancellationToken cancellationToken)
+    public async Task HandleAsync(PaymentProcessingRequested message, IMessageContext messageContext, CancellationToken cancellationToken)
     {
@@
-            await eventPublisher.PublishAsync(new PaymentCompletedEvent
+            await messageContext.PublishAsync(new PaymentCompletedEvent
             {
@@
-            }, cancellationToken);
+            });
@@
-            await eventPublisher.PublishAsync(new PaymentFailedEvent
+            await messageContext.PublishAsync(new PaymentFailedEvent
             {
@@
-            }, cancellationToken);
+            });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@PaymentService/Features/ProcessPayment.cs` around lines 234 - 271, The
terminal payment events (PaymentCompletedEvent in the success branch and
PaymentFailedEvent in the failure branch) are being published through the
constructor-injected eventPublisher before SaveChangesAsync, but in Wolverine
6.x this publisher is not transaction-enlisted, creating a risk where consumers
observe these events even if SaveChangesAsync later fails and the database
changes don't commit. Replace the eventPublisher.PublishAsync calls in both the
success and failure branches with a transaction-enlisted publish mechanism
(likely through the context object) to ensure atomicity with the database
changes, so the events are only visible to consumers if and when
SaveChangesAsync succeeds.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.codacy/cli.sh:
- Line 40: The local variable declaration combined with command substitution
assignment masks the exit code of the pipeline containing grep and cut. Separate
the local declaration from the assignment of the version variable so that the
return value of the command substitution is properly exposed and can be checked
for errors. This allows detection if grep or cut fails during extraction of the
version string from the version_file.
- Line 58: The local variable declaration and assignment on line 58 using the
pattern `local version=$(...)` masks the exit status of the command
substitution, preventing detection of failures from the echo, grep, or cut
commands. Split the declaration and assignment into two separate statements:
first declare `local version` on its own line, then assign the result of the
command substitution `version=$(echo "$response" | grep -m 1 tag_name | cut
-d'"' -f4)` on the next line so that the exit code from the pipeline is properly
propagated and can be checked.
- Around line 26-28: The conditional checks on lines 26 and 28 are calling
`uname` multiple times when the `os_name` variable was already set on line 10.
Replace the `$(uname)` calls in the conditional expressions with references to
the cached `$os_name` variable to eliminate the redundant system calls and
improve efficiency.
- Around line 49-60: The get_latest_version() function lacks error handling
after the curl calls, meaning network failures, HTTP errors, or invalid
responses will silently result in an empty version string. Add explicit
validation immediately after the curl assignments to check that the response is
not empty and contains valid JSON. If the response is invalid or empty, either
return an error code or log a warning before attempting to extract the version
with the grep command on line 58. This ensures that failures in fetching from
the GitHub API are explicitly detected rather than producing empty version
strings that propagate silently.
- Line 148: Replace the eval command in the cli.sh script that currently uses
`eval "$run_command $*"`. First, change `$*` to `"$@"` to preserve individual
argument boundaries. If `$run_command` contains only a binary path or command
name without dynamic code, remove the eval wrapper entirely and execute the
command directly as `$run_command "$@"`. If eval is truly necessary for some
reason, at minimum replace `$*` with `"$@"` to ensure arguments are passed as
separate tokens rather than concatenated strings, which prevents word-splitting
issues and improves security.
- Line 65: The `fatal` function is called at three locations in .codacy/cli.sh
(lines 65, 78, and 142) but is never defined, causing runtime crashes when those
error conditions are triggered. Define the `fatal` function once near the top of
the script after line 7. This single definition will resolve all three call
sites. The function should output the error message and cleanly exit the script,
ensuring the rate limit error at line 65, the missing curl/wget error at line
78, and the empty binary path error at line 142 all report clean errors instead
of crashing with "command not found".
- Line 4: In `.codacy/cli.sh` at line 4, change `set -e +o pipefail` to `set -e
-o pipefail` (replace the `+o` with `-o`). The `+o pipefail` syntax disables
pipeline failure detection, allowing malformed API responses to be silently
processed. After making this change, add explicit validation checks in the
`get_latest_version()` function (around line 58) to verify that the piped
command result is not empty before using the extracted version value, ensuring
that empty or malformed API responses are properly caught and handled.
- Around line 97-105: Add SHA256 checksum validation to protect against
supply-chain attacks when downloading the Codacy CLI binary. After downloading
the tarball using the download function and before extracting it with the tar
command, download the checksums.txt file from the same GitHub release URL (using
the pattern codacy-cli-v2_${version}_checksums.txt), then validate the
downloaded tarball using sha256sum with the --ignore-missing flag before
proceeding with the tar extraction. This ensures the binary integrity is
verified prior to extraction.

In @.codacy/codacy.yaml:
- Around line 1-15: The runtimes and tools sections in the Codacy configuration
file are pinned to older versions (such as go@1.24.13, dart@3.7.2, java@17.0.10,
node@22.2.0, python@3.11.11, and various analysis tools like eslint, pylint,
trivy, etc.). Review Codacy's supported runtime matrix documentation to identify
the current stable releases for each runtime and tool, then update all version
pins in both the runtimes and tools sections to their latest stable versions to
improve long-term security and feature support.

In `@CLAUDE.md`:
- Around line 240-241: The CLAUDE.md file exceeds its 500-line hard limit due to
an overly detailed Wolverine upgrade explanation added at lines 240-241. Replace
this verbose explanation with a concise one-liner rule stating that handlers
must use method-injected IMessageContext (not constructor-injected
IEventPublisher) for transactional message publishing, and move the full
detailed explanation including the PaymentService example, the rationale about
transaction enlistment, and references to the upgrade notes to
docs/project-decisions.md under the "Wolverine 5→6 upgrade" section where
detailed context belongs.

---

Outside diff comments:
In `@PaymentService/Features/ProcessPayment.cs`:
- Around line 234-271: The terminal payment events (PaymentCompletedEvent in the
success branch and PaymentFailedEvent in the failure branch) are being published
through the constructor-injected eventPublisher before SaveChangesAsync, but in
Wolverine 6.x this publisher is not transaction-enlisted, creating a risk where
consumers observe these events even if SaveChangesAsync later fails and the
database changes don't commit. Replace the eventPublisher.PublishAsync calls in
both the success and failure branches with a transaction-enlisted publish
mechanism (likely through the context object) to ensure atomicity with the
database changes, so the events are only visible to consumers if and when
SaveChangesAsync succeeds.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 5cd3d326-af3f-474b-9c5f-d8e9200409b2

📥 Commits

Reviewing files that changed from the base of the PR and between 1042e86 and 61436f9.

⛔ Files ignored due to path filters (1)
  • .claude/scheduled_tasks.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • .codacy/cli.sh
  • .codacy/codacy.yaml
  • CLAUDE.md
  • CatalogService/Program.cs
  • Directory.Packages.props
  • NextAurora.ServiceDefaults/Extensions.cs
  • NextAurora.ServiceDefaults/NextAurora.ServiceDefaults.csproj
  • NotificationService/Program.cs
  • OrderService/Infrastructure/WolverineEventPublisher.cs
  • OrderService/Program.cs
  • PaymentService/Features/ProcessPayment.cs
  • PaymentService/Infrastructure/WolverineEventPublisher.cs
  • PaymentService/Program.cs
  • ShippingService/Infrastructure/WolverineEventPublisher.cs
  • ShippingService/Program.cs
  • docs/STATUS.md
  • docs/project-decisions.md

Comment thread .codacy/cli.sh Outdated
#!/usr/bin/env bash


set -e +o pipefail

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify if there are pipelines in this script that might fail benignly
rg -n '\|' .codacy/cli.sh

Repository: emeraldleaf/NextAurora

Length of output: 446


🏁 Script executed:

cat -n .codacy/cli.sh

Repository: emeraldleaf/NextAurora

Length of output: 4917


Replace +o pipefail with -o pipefail to catch pipeline failures.

The +o pipefail syntax disables pipefail, masking failures in command pipelines. This creates a real issue: at line 58, get_latest_version() uses echo "$response" | grep -m 1 tag_name | cut -d'"' -f4 without validating the result. If the API response is empty or malformed, grep fails but the pipeline returns 0 (from cut), resulting in an empty version being written to version.yaml.

Change line 4 to set -e -o pipefail to properly catch pipeline errors, then add explicit validation checks in get_latest_version() for empty results before using the version.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.codacy/cli.sh at line 4, In `.codacy/cli.sh` at line 4, change `set -e +o
pipefail` to `set -e -o pipefail` (replace the `+o` with `-o`). The `+o
pipefail` syntax disables pipeline failure detection, allowing malformed API
responses to be silently processed. After making this change, add explicit
validation checks in the `get_latest_version()` function (around line 58) to
verify that the piped command result is not empty before using the extracted
version value, ensuring that empty or malformed API responses are properly
caught and handled.

Comment thread .codacy/cli.sh Outdated
Comment on lines +26 to +28
if [ "$(uname)" = "Linux" ]; then
CODACY_CLI_V2_TMP_FOLDER="$HOME/.cache/codacy/codacy-cli-v2"
elif [ "$(uname)" = "Darwin" ]; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Use the cached os_name variable.

Lines 26 and 28 call uname again, but os_name was already set on Line 10. This is a minor inefficiency.

♻️ Proposed refactor
-    if [ "$(uname)" = "Linux" ]; then
+    if [ "$os_name" = "Linux" ]; then
         CODACY_CLI_V2_TMP_FOLDER="$HOME/.cache/codacy/codacy-cli-v2"
-    elif [ "$(uname)" = "Darwin" ]; then
+    elif [ "$os_name" = "Darwin" ]; then
         CODACY_CLI_V2_TMP_FOLDER="$HOME/Library/Caches/Codacy/codacy-cli-v2"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if [ "$(uname)" = "Linux" ]; then
CODACY_CLI_V2_TMP_FOLDER="$HOME/.cache/codacy/codacy-cli-v2"
elif [ "$(uname)" = "Darwin" ]; then
if [ "$os_name" = "Linux" ]; then
CODACY_CLI_V2_TMP_FOLDER="$HOME/.cache/codacy/codacy-cli-v2"
elif [ "$os_name" = "Darwin" ]; then
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.codacy/cli.sh around lines 26 - 28, The conditional checks on lines 26 and
28 are calling `uname` multiple times when the `os_name` variable was already
set on line 10. Replace the `$(uname)` calls in the conditional expressions with
references to the cached `$os_name` variable to eliminate the redundant system
calls and improve efficiency.

Comment thread .codacy/cli.sh Outdated
Comment thread .codacy/cli.sh Outdated
Comment on lines +49 to +60
get_latest_version() {
local response
if [ -n "$GH_TOKEN" ]; then
response=$(curl -Lq --header "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
else
response=$(curl -Lq "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
fi

handle_rate_limit "$response"
local version=$(echo "$response" | grep -m 1 tag_name | cut -d'"' -f4)
echo "$version"
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add error handling for GitHub API failures.

The function only checks for rate limiting but doesn't handle network errors, 404 responses, or invalid JSON. If curl fails, $response will be empty, and Line 58 will produce an empty version string.

Consider adding explicit error checks after the curl call.

🛡️ Suggested improvement
     if [ -n "$GH_TOKEN" ]; then
-        response=$(curl -Lq --header "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
+        response=$(curl -Lq --fail --header "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null) || {
+            echo "Error: Failed to fetch latest version from GitHub API" >&2
+            return 1
+        }
     else
-        response=$(curl -Lq "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
+        response=$(curl -Lq --fail "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null) || {
+            echo "Error: Failed to fetch latest version from GitHub API" >&2
+            return 1
+        }
     fi
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
get_latest_version() {
local response
if [ -n "$GH_TOKEN" ]; then
response=$(curl -Lq --header "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
else
response=$(curl -Lq "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null)
fi
handle_rate_limit "$response"
local version=$(echo "$response" | grep -m 1 tag_name | cut -d'"' -f4)
echo "$version"
}
get_latest_version() {
local response
if [ -n "$GH_TOKEN" ]; then
response=$(curl -Lq --fail --header "Authorization: Bearer $GH_TOKEN" "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null) || {
echo "Error: Failed to fetch latest version from GitHub API" >&2
return 1
}
else
response=$(curl -Lq --fail "https://api.github.com/repos/codacy/codacy-cli-v2/releases/latest" 2>/dev/null) || {
echo "Error: Failed to fetch latest version from GitHub API" >&2
return 1
}
fi
handle_rate_limit "$response"
local version=$(echo "$response" | grep -m 1 tag_name | cut -d'"' -f4)
echo "$version"
}
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 58-58: Declare and assign separately to avoid masking return values.

(SC2155)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.codacy/cli.sh around lines 49 - 60, The get_latest_version() function lacks
error handling after the curl calls, meaning network failures, HTTP errors, or
invalid responses will silently result in an empty version string. Add explicit
validation immediately after the curl assignments to check that the response is
not empty and contains valid JSON. If the response is invalid or empty, either
return an error code or log a warning before attempting to extract the version
with the grep command on line 58. This ensures that failures in fetching from
the GitHub API are explicitly detected rather than producing empty version
strings that propagate silently.

Comment thread .codacy/cli.sh Outdated
Comment thread .codacy/cli.sh Outdated
Comment thread .codacy/cli.sh Outdated
Comment thread .codacy/cli.sh Outdated
Comment thread .codacy/codacy.yaml Outdated
Comment thread CLAUDE.md Outdated
…cted IMessageContext

Completes the Wolverine 6 outbox-atomicity fix across all services. The constructor-injected
IEventPublisher (over IMessageBus) is NOT enlisted in the handler's outbox transaction under 6.x —
a publish through it fires inline, before SaveChanges commits, silently defeating the transactional
outbox. Proven with a rollback test (force the commit to fail; assert no event dispatched):
OrderService's external OrderPlacedEvent survived a rolled-back commit — non-atomic.

Fix: publish through the IMessageContext Wolverine injects as a HandleAsync method parameter (the
only enlisted context). Applied to every write-then-publish handler:
- OrderService PlaceOrderHandler  → OrderPlacedEvent
- ShippingService CreateShipmentHandler → ShipmentDispatchedEvent
- PaymentService Gateway (PaymentProcessingRequestedHandler) → PaymentCompleted/FailedEvent

Dead-code removal: the IEventPublisher shim is now unused by OrderService and ShippingService
handlers (only consumers) → deleted port + impl + registration in both. Retained in PaymentService
for two non-write-then-publish paths: the Acceptor's terminal-event REPUBLISH (already-committed
payment, no entity write → inline send is correct) and the PaymentRecoveryJob.

Test: new ThrowingSaveChangesInterceptor + OrderSagaTests.PlaceOrder_does_not_dispatch_
OrderPlacedEvent_when_the_commit_rolls_back (RED before fix, GREEN after; first assertion guards
that the interceptor actually fired, so no false pass).

Docs: full war story in docs/war-story-wolverine6-outbox-atomicity.md; project-decisions.md §13
updated (caveat → proven+fixed). Still open (tracked): PaymentRecoveryJob non-handler enlistment
needs IDbContextOutbox + its own rollback test.

Tests: build clean (0 warnings); 116 unit/arch + 39 integration green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@emeraldleaf emeraldleaf changed the title chore(deps): upgrade Wolverine 5.39.3 → 6.8.0 Wolverine 5→6 upgrade + transactional-outbox atomicity fix Jun 15, 2026
coderabbitai[bot]
coderabbitai Bot previously requested changes Jun 15, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/OrderService.Tests.Integration/OrderSagaTests.cs`:
- Around line 121-127: The test currently does not explicitly verify that the
POST request failed as expected before checking DB and event invariants, which
allows false-positive passes if the request fails earlier for a different
reason. Modify the ExecuteAndWaitAsync call to capture the HTTP response from
client.PostAsJsonAsync along with the session, then add an explicit assertion on
the response status code to confirm it contains the expected failure status
(such as an error code indicating SaveChanges failure) before proceeding with
any DB or event invariant checks. This ensures the failure occurred in the
specific SaveChanges path being tested, not in some other path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 95530f9b-8664-432c-b61b-10fc2b14b753

📥 Commits

Reviewing files that changed from the base of the PR and between 61436f9 and 7ec17de.

📒 Files selected for processing (13)
  • OrderService/Domain/IEventPublisher.cs
  • OrderService/Features/PlaceOrder.cs
  • OrderService/Infrastructure/DependencyInjection.cs
  • OrderService/Infrastructure/WolverineEventPublisher.cs
  • PaymentService/Features/ProcessPayment.cs
  • ShippingService/Domain/IEventPublisher.cs
  • ShippingService/Features/CreateShipment.cs
  • ShippingService/Infrastructure/DependencyInjection.cs
  • ShippingService/Infrastructure/WolverineEventPublisher.cs
  • docs/project-decisions.md
  • docs/war-story-wolverine6-outbox-atomicity.md
  • tests/OrderService.Tests.Integration/OrderSagaTests.cs
  • tests/OrderService.Tests.Integration/ThrowingSaveChangesInterceptor.cs
💤 Files with no reviewable changes (4)
  • ShippingService/Domain/IEventPublisher.cs
  • OrderService/Domain/IEventPublisher.cs
  • ShippingService/Infrastructure/WolverineEventPublisher.cs
  • OrderService/Infrastructure/WolverineEventPublisher.cs

Comment thread tests/OrderService.Tests.Integration/OrderSagaTests.cs
emeraldleaf and others added 2 commits June 14, 2026 19:23
…ses the non-handler case)

The recovery sweeper marks timed-out payments Failed and publishes PaymentFailedEvent outside the
Wolverine handler pipeline, inside an explicit BeginTransactionAsync. Under 6.x the constructor-injected
IEventPublisher (IMessageBus) isn't enlisted in that transaction, so the event published INLINE — a
rolled-back recovery commit still dispatched PaymentFailedEvent (a "payment failed" event for a payment
whose Failed state never persisted). Same root cause as the handlers, different fix (no method-injected
context available in a BackgroundService).

Fix: Wolverine's non-handler outbox API — enroll the DbContext in an IDbContextOutbox, publish through
it, then SaveChangesAndFlushMessagesAsync() (stages envelope + saves entity + commits, atomically).
Replaces the manual BeginTransaction → Publish → SaveChanges → Commit. IDbContextOutbox resolves from
the existing AddDbContext + UseEntityFrameworkCoreTransactions() registration — no wiring change.

Proven by the "outbox-in-non-handler" rollback test CLAUDE.md requires (and which didn't exist):
PaymentRecoveryAtomicityTests.RecoverySweep_does_not_dispatch_PaymentFailedEvent_when_the_commit_rolls_back
+ ThrowOnPaymentUpdateInterceptor — RED before, GREEN after (first assertion guards the interceptor fired).
InternalsVisibleTo added for the integration project to drive internal SweepAsync.

IEventPublisher is retained in PaymentService only for the Acceptor's terminal-event republish (an
already-committed payment, no entity write → inline send is correct).

Docs: project-decisions.md §13 + war-story updated (RecoveryJob caveat → fixed). Closes #153.

Tests: build clean (0 warnings); 116 unit/arch + 40 integration green.

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

CI build was failing on the broken-link audit: docs/code-flows/{order,shipping}service.md still
linked to the OrderService/ShippingService IEventPublisher + WolverineEventPublisher files this PR
deleted. Updated both code-flow docs — the mermaid publish participant is now IMessageContext
(method-injected), and the dead-file table rows are replaced with the framework-type note. Fixed the
matching stale (non-link) reference in docs/observability.md.

Untracked .codacy/ (Codacy CLI artifacts accidentally committed via `git add -A`) and added it to
.gitignore — restores its pre-session untracked state; also clears CodeRabbit's noise on cli.sh.

CodeRabbit feedback:
- CLAUDE.md: trimmed the new Wolverine rule to a headline + one-line summary + pointers (the worked
  example/rationale already live in project-decisions.md + the war story). CodeRabbit's "over 500
  lines" premise was wrong (file is 319), but the lean-CLAUDE.md convention favors the trim anyway.
- OrderSagaTests rollback test: assert the POST status so it can't false-pass if the request fails
  before the forced commit. The interceptor now throws DbUpdateException (a real infra commit failure
  → GlobalExceptionHandler 500), distinct from the 409 it gives business conflicts; test asserts 500.

Verified locally: broken-link audit fail=0, CLAUDE.md 319 lines, OrderService integration green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@emeraldleaf
emeraldleaf dismissed coderabbitai[bot]’s stale review June 15, 2026 02:03

addressed in later commits

@emeraldleaf
emeraldleaf merged commit 4f87c68 into main Jun 15, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PaymentRecoveryJob: outbox atomicity for non-handler publishing under Wolverine 6

1 participant