Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 71 additions & 8 deletions .github/workflows/ci-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ on:
options:
- Debug
- Release
run_mac_tests:
type: boolean
description: Run the macOS test matrix despite the additional cost and runtime.
default: false

permissions:
contents: read
Expand All @@ -22,13 +26,20 @@ jobs:
runs-on: ubuntu-24.04
outputs:
run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }}
run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }}
strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }}
build-switches: ${{ steps.vars.outputs.build-switches }}
steps:
- id: vars
name: calculate workflow variables
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ inputs.run_mac_tests }}" == "true" ]]; then
echo "run-mac-tests=true" >> "$GITHUB_OUTPUT"
else
echo "run-mac-tests=false" >> "$GITHUB_OUTPUT"
fi

if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.event.pull_request.head.repo.full_name }}" != "${{ github.repository }}" ]]; then
echo "run-privileged-jobs=false" >> "$GITHUB_OUTPUT"
echo "strong-name-key-filename=" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -130,8 +141,9 @@ jobs:
download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }}

test_mac:
if: ${{ needs.init.outputs.run-mac-tests == 'true' }}
name: call-test-mac
needs: [build, prepare_test]
needs: [init, build, prepare_test]
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -256,10 +268,61 @@ jobs:
docker stop nats
docker rm nats

test_qualitygate:
if: ${{ always() }}
name: test-qualitygate
needs: [init, test_linux, test_windows, test_mac, integration_test, integration_test_rabbitmq, integration_test_nats]
runs-on: ubuntu-24.04
steps:
- name: Evaluate test results
shell: bash
env:
RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }}
RUN_PRIVILEGED_JOBS: ${{ needs.init.outputs.run-privileged-jobs }}
TEST_LINUX_RESULT: ${{ needs.test_linux.result }}
TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }}
TEST_MAC_RESULT: ${{ needs.test_mac.result }}
INTEGRATION_TEST_RESULT: ${{ needs.integration_test.result }}
INTEGRATION_TEST_RABBITMQ_RESULT: ${{ needs.integration_test_rabbitmq.result }}
INTEGRATION_TEST_NATS_RESULT: ${{ needs.integration_test_nats.result }}
run: |
require_success() {
local job_name="$1"
local job_result="$2"

if [[ "$job_result" != "success" ]]; then
echo "::error::$job_name finished with '$job_result'."
exit 1
fi
}

require_success_or_skip() {
local job_name="$1"
local job_enabled="$2"
local job_result="$3"

if [[ "$job_enabled" == "true" ]]; then
require_success "$job_name" "$job_result"
return
fi

if [[ "$job_result" != "success" && "$job_result" != "skipped" ]]; then
echo "::error::$job_name finished with '$job_result' while disabled."
exit 1
fi
}

require_success "test_linux" "$TEST_LINUX_RESULT"
require_success "test_windows" "$TEST_WINDOWS_RESULT"
require_success_or_skip "test_mac" "$RUN_MAC_TESTS" "$TEST_MAC_RESULT"
require_success_or_skip "integration_test" "$RUN_PRIVILEGED_JOBS" "$INTEGRATION_TEST_RESULT"
require_success "integration_test_rabbitmq" "$INTEGRATION_TEST_RABBITMQ_RESULT"
require_success "integration_test_nats" "$INTEGRATION_TEST_NATS_RESULT"
Comment on lines +319 to +320

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 RabbitMQ/NATS jobs are always required but have no skip path

integration_test_rabbitmq and integration_test_nats have no if: guard, so they always run on every trigger (including fork pull requests). require_success is therefore the right gate for them. However, if either job is ever given a conditional guard in the future, this call site would need to be upgraded to require_success_or_skip — unlike integration_test, there is no _ENABLED flag wired here. Worth keeping this asymmetry in mind if the RabbitMQ or NATS jobs are later made optional.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/ci-pipeline.yml
Line: 319-320

Comment:
**RabbitMQ/NATS jobs are always required but have no skip path**

`integration_test_rabbitmq` and `integration_test_nats` have no `if:` guard, so they always run on every trigger (including fork pull requests). `require_success` is therefore the right gate for them. However, if either job is ever given a conditional guard in the future, this call site would need to be upgraded to `require_success_or_skip` — unlike `integration_test`, there is no `_ENABLED` flag wired here. Worth keeping this asymmetry in mind if the RabbitMQ or NATS jobs are later made optional.

How can I resolve this? If you propose a fix, please make it concise.


sonarcloud:
if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}}
name: call-sonarcloud
needs: [init, build, test_linux, test_windows, test_mac, integration_test, integration_test_rabbitmq, integration_test_nats]
needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3
with:
organization: geekle
Expand All @@ -268,26 +331,26 @@ jobs:
secrets: inherit

codecov:
if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}}
name: call-codecov
needs: [init, build, test_linux, test_windows, test_mac, integration_test, integration_test_rabbitmq, integration_test_nats]
needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1
with:
repository: codebeltnet/savvyio
secrets: inherit

codeql:
if: ${{ needs.init.outputs.run-privileged-jobs == 'true' }}
if: ${{always() && needs.init.outputs.run-privileged-jobs == 'true' && needs.build.result == 'success' && needs.test_qualitygate.result == 'success'}}
name: call-codeql
needs: [init, build, test_linux, test_windows, test_mac, integration_test, integration_test_rabbitmq, integration_test_nats]
needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3
permissions:
security-events: write

deploy:
if: github.event_name != 'pull_request'
name: call-nuget
needs: [build, pack, test_linux, test_windows, test_mac, integration_test, integration_test_rabbitmq, integration_test_nats, sonarcloud, codecov, codeql]
needs: [build, pack, test_qualitygate, sonarcloud, codecov, codeql]
uses: codebeltnet/jobs-nuget-push/.github/workflows/default.yml@v3
with:
version: ${{ needs.build.outputs.version }}
Expand Down
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,29 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

For more details, please refer to `PackageReleaseNotes.txt` on a per assembly basis in the `.nuget` folder.

## [Unreleased]

This is a patch release focused on consolidating the internal `AssemblyContext` reflection utility into the upstream `Cuemon.Reflection` package and tightening the CI pipeline structure.

> [!TIP]
> The `Savvyio.Reflection.AssemblyContext` class has been removed from the `Savvyio.Core` assembly. Consumers referencing it directly must migrate to `Cuemon.Reflection.AssemblyContext` and replace the `CurrentDomainAssemblies` property with the `GetCurrentDomainAssemblies()` method.

### Changed

- `SavvyioOptionsExtensions` in `Savvyio.Extensions.Dispatchers` now calls `Cuemon.Reflection.AssemblyContext.GetCurrentDomainAssemblies()` to replace the removed `Savvyio.Reflection.AssemblyContext.CurrentDomainAssemblies` property,
- `MessageConverter` in `Savvyio.Extensions.Newtonsoft.Json` updated to use `Cuemon.Reflection.AssemblyContext.GetCurrentDomainAssemblies()`,
- `MessageConverter` in `Savvyio.Extensions.Text.Json` updated to use `Cuemon.Reflection.AssemblyContext.GetCurrentDomainAssemblies()`,
- LocalStack Docker image bumped from `4.14.0` to `2026.05.0` in `Dockerfile.localstack` and `docker-compose.yml`,
- CI pipeline now supports an opt-in `run_mac_tests` workflow dispatch boolean (default `false`) to run the macOS test matrix on demand rather than always,
- macOS test job (`test_mac`) is now guarded by the `run-mac-tests` output and requires `init` as an explicit dependency,
- New `test_qualitygate` job centralises evaluation of all test results (Linux, Windows, macOS, integration, RabbitMQ, NATS) using `require_success` and `require_success_or_skip` helper functions,
- `sonarcloud`, `codecov`, `codeql`, and `deploy` jobs now depend on `test_qualitygate` instead of enumerating each individual test job.

### Removed

- `AssemblyContext` class from the `Savvyio.Core` assembly (`Savvyio.Reflection` namespace); functionality is consolidated into `Cuemon.Reflection.AssemblyContext`,
- `AssemblyContextTest` unit tests removed alongside the deleted class.

## [5.0.7] - 2026-05-26

This is a patch release focused on Azure.Identity compatibility across target frameworks, RabbitMQ queue durability correction, comprehensive test coverage expansion across multiple extensions, testability improvements with protected virtual methods and constructors for extensibility, dependency updates including LocalStack, NATS.Client, and Microsoft utility packages, and test reliability hardening for distributed mediator scenarios.
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile.localstack
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Use the LocalStack base image
FROM localstack/localstack:4.14.0
FROM localstack/localstack:2026.05.0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Large LocalStack version jump may affect AWS service emulation

The image moves from 4.14.0 to 2026.05.0, which appears to be LocalStack's calendar versioning scheme and represents a very large upstream delta. The integration tests exercise sns and sqs; any breaking changes in request/response shapes, endpoint behaviour, or error codes would only surface at test time. If the integration test suite passes cleanly with this image, the bump is safe — just worth being aware of the scope of the change.

Prompt To Fix With AI
This is a comment left during a code review.
Path: Dockerfile.localstack
Line: 2

Comment:
**Large LocalStack version jump may affect AWS service emulation**

The image moves from `4.14.0` to `2026.05.0`, which appears to be LocalStack's calendar versioning scheme and represents a very large upstream delta. The integration tests exercise `sns` and `sqs`; any breaking changes in request/response shapes, endpoint behaviour, or error codes would only surface at test time. If the integration test suite passes cleanly with this image, the bump is safe — just worth being aware of the scope of the change.

How can I resolve this? If you propose a fix, please make it concise.


# Expose the port for LocalStack
EXPOSE 4566
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
services:
localstack:
image: localstack/localstack:4.14.0
image: localstack/localstack:2026.05.0
environment:
- SERVICES=sns,sqs
- DEBUG=0
Expand Down
1 change: 0 additions & 1 deletion src/Savvyio.Core/Reflection/AssemblyContext.cs

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
using Savvyio.Commands;
using Cuemon.Reflection;
using Savvyio.Commands;
using Savvyio.Dispatchers;
using Savvyio.Domain;
using Savvyio.EventDriven;
using Savvyio.Queries;
using System.Linq;
using System.Reflection;
using Savvyio.Reflection;
using System.Runtime.CompilerServices;

namespace Savvyio.Extensions
Expand Down Expand Up @@ -43,7 +43,7 @@ public static SavvyioOptions UseAutomaticDispatcherDiscovery(this SavvyioOptions
{
if (bruteAssemblyScanning)
{
options.AddDispatchers(AssemblyContext.CurrentDomainAssemblies.ToArray());
options.AddDispatchers(AssemblyContext.GetCurrentDomainAssemblies().ToArray());
}
else
{
Expand All @@ -63,7 +63,7 @@ public static SavvyioOptions UseAutomaticHandlerDiscovery(this SavvyioOptions op
{
if (bruteAssemblyScanning)
{
options.AddHandlers(AssemblyContext.CurrentDomainAssemblies.ToArray());
options.AddHandlers(AssemblyContext.GetCurrentDomainAssemblies().ToArray());
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Linq;
using System.Reflection;
using Cuemon.Extensions;
using Cuemon.Reflection;
using Codebelt.Extensions.Newtonsoft.Json;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
Expand All @@ -11,7 +12,6 @@
using Savvyio.EventDriven.Messaging.CloudEvents.Cryptography;
using Savvyio.Messaging;
using Savvyio.Messaging.Cryptography;
using Savvyio.Reflection;

namespace Savvyio.Extensions.Newtonsoft.Json.Converters
{
Expand All @@ -21,7 +21,7 @@ namespace Savvyio.Extensions.Newtonsoft.Json.Converters
/// <seealso cref="JsonConverter" />
public class MessageConverter : JsonConverter
{
internal static readonly Lazy<IList<TypeInfo>> CloudEventTypes = new(() => AssemblyContext.CurrentDomainAssemblies.SelectMany(a => a.DefinedTypes.Where(ti => ti.HasInterfaces(typeof(ICloudEvent<>)) &&
internal static readonly Lazy<IList<TypeInfo>> CloudEventTypes = new(() => AssemblyContext.GetCurrentDomainAssemblies().SelectMany(a => a.DefinedTypes.Where(ti => ti.HasInterfaces(typeof(ICloudEvent<>)) &&
ti is { IsAbstract: false, IsInterface: false })).ToList());

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,13 @@
using System.Text.Json.Serialization;
using Cuemon.Extensions;
using Cuemon.Extensions.Reflection;
using Cuemon.Reflection;
using Cuemon.Extensions.Text.Json;
using Savvyio.EventDriven;
using Savvyio.EventDriven.Messaging.CloudEvents;
using Savvyio.EventDriven.Messaging.CloudEvents.Cryptography;
using Savvyio.Messaging;
using Savvyio.Messaging.Cryptography;
using Savvyio.Reflection;

namespace Savvyio.Extensions.Text.Json.Converters
{
Expand All @@ -22,7 +22,7 @@
/// <seealso cref="JsonConverter" />
public class MessageConverter : JsonConverterFactory
{
internal static readonly Lazy<IList<TypeInfo>> CloudEventTypes = new(() => AssemblyContext.CurrentDomainAssemblies.SelectMany(a => a.DefinedTypes.Where(ti => ti.HasInterfaces(typeof(ICloudEvent<>)) &&
internal static readonly Lazy<IList<TypeInfo>> CloudEventTypes = new(() => AssemblyContext.GetCurrentDomainAssemblies().SelectMany(a => a.DefinedTypes.Where(ti => ti.HasInterfaces(typeof(ICloudEvent<>)) &&
ti is { IsAbstract: false, IsInterface: false })).ToList());

/// <summary>
Expand Down Expand Up @@ -67,7 +67,7 @@
{
}

public override IMessage<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)

Check warning on line 70 in src/Savvyio.Extensions.Text.Json/Converters/MessageConverter.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

Check warning on line 70 in src/Savvyio.Extensions.Text.Json/Converters/MessageConverter.cs

View workflow job for this annotation

GitHub Actions / call-sonarcloud / 🔬 Code Quality Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.
{
using (var document = JsonDocument.ParseValue(ref reader))
{
Expand Down
Loading
Loading