From ce542dcb683c2c4ff1835e547d66e22b0d548d9d Mon Sep 17 00:00:00 2001 From: Neal Mummau Date: Mon, 19 Oct 2026 21:53:43 -0400 Subject: [PATCH 1/6] ci: add reusable code style verification workflow Run dotnet format style checks on pull requests and pushes to dev. Fail verification when required project references cannot be loaded, and require code style checks to pass before preview publishing. Document local verification and formatting commands for contributors issue #624 --- .github/workflows/code-style.yml | 37 ++++++++++++++++++++++++++++++ .github/workflows/preview.yml | 6 ++++- .github/workflows/pull-request.yml | 4 ++++ CONTRIBUTING.md | 18 +++++++++++++++ 4 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/code-style.yml diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml new file mode 100644 index 000000000..ec69fb1f6 --- /dev/null +++ b/.github/workflows/code-style.yml @@ -0,0 +1,37 @@ +name: Code style + +on: + workflow_call: + workflow_dispatch: + +jobs: + verify: + name: Verify code style + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + env: + DOTNET_CLI_UI_LANGUAGE: en-US + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Setup .NET + uses: actions/setup-dotnet@v6 + with: + dotnet-version: | + 8.0.x + 9.0.x + 10.0.x + - name: Restore solution + run: dotnet restore Eventuous.slnx + - name: Verify code style + shell: bash + run: | + set -euo pipefail + log_file="$RUNNER_TEMP/dotnet-format.log" + dotnet format style Eventuous.slnx --verify-no-changes --no-restore --severity warn --verbosity diagnostic 2>&1 | tee "$log_file" + if grep -Fq 'Required references did not load for ' "$log_file"; then + echo "::error::Code style verification was incomplete because project references could not be loaded." + exit 1 + fi diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index 12822a0de..ac61ed572 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -9,6 +9,10 @@ on: - '*.md' jobs: + code-style: + name: Code style + uses: ./.github/workflows/code-style.yml + event_file: # Consumed by the Test Results workflow (test-results.yml) to publish results on the commit. name: "Event File" @@ -28,7 +32,7 @@ jobs: nuget: runs-on: ubuntu-latest - needs: tests + needs: [ tests, code-style ] permissions: id-token: write contents: read diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index ea3cfac7c..4e6fa3de6 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -11,6 +11,10 @@ concurrency: cancel-in-progress: true jobs: + code-style: + name: Code style + uses: ./.github/workflows/code-style.yml + event_file: # Consumed by the Test Results workflow (test-results.yml) to publish results with the # correct PR context. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5d952ad46..430d11011 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,4 +9,22 @@ Here are simple rules that we expect to be followed by contributors: - Use the code formats of the repository, we have them in the `.editorconfig` file. Reformat the code accordingly if you copy it from somewhere. - Only contribute with your own work, or if you have a permission from the others to contribute their code. +## Code style checks + +The reusable [code-style workflow](.github/workflows/code-style.yml) runs for pull requests and pushes to `dev`. + +Run the same style verification locally from the repository root: + +```sh +dotnet format style Eventuous.slnx --verify-no-changes --severity warn +``` + +This checks Roslyn code-style rules at warning or error severity, including naming conventions and unnecessary imports. Apply available fixes with: + +```sh +dotnet format style Eventuous.slnx --severity warn +``` + +Some violations, such as naming-rule violations, require manual fixes. Rerun verification after making changes. CI also fails if the formatter reports that required project references could not be loaded. + Thanks for contributing! From 7e3e8d4390930dc28394d87895f916aa8e04d8ed Mon Sep 17 00:00:00 2001 From: Neal Mummau Date: Mon, 19 Oct 2026 21:30:46 -0400 Subject: [PATCH 2/6] fix: resolve IDE1006 naming rule violations Rename static fields and the StreamReadPosition constructor parameter to match the repository's naming conventions. issue #624 --- .../src/Eventuous.Diagnostics/EventuousDiagnostics.cs | 8 ++++---- .../src/Eventuous.Persistence/ExpectedStreamVersion.cs | 6 +++--- src/Core/src/Eventuous.Serialization/EventSerializer.cs | 8 ++++---- src/Core/src/Eventuous.Shared/TypeMap/TypeMap.cs | 7 +++---- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/Core/src/Eventuous.Diagnostics/EventuousDiagnostics.cs b/src/Core/src/Eventuous.Diagnostics/EventuousDiagnostics.cs index a5c6a58b0..5d65b56b9 100644 --- a/src/Core/src/Eventuous.Diagnostics/EventuousDiagnostics.cs +++ b/src/Core/src/Eventuous.Diagnostics/EventuousDiagnostics.cs @@ -9,13 +9,13 @@ public static class EventuousDiagnostics { static EventuousDiagnostics() { Enabled = Environment.GetEnvironmentVariable("EVENTUOUS_DISABLE_DIAGS") != "1"; ActivitySource = new(InstrumentationName, InstrumentationVersion.Value); - listener = DummyActivityListener.Create(); - ActivitySource.AddActivityListener(listener); + Listener = DummyActivityListener.Create(); + ActivitySource.AddActivityListener(Listener); } public const string InstrumentationName = DiagnosticName.BaseName; - static readonly ActivityListener listener; + static readonly ActivityListener Listener; public static KeyValuePair[] Tags { get; private set; } = []; @@ -46,7 +46,7 @@ public static void AddDefaultTag(string key, object? value) { public static ActivitySource ActivitySource { get; } - public static void RemoveDummyListener() => listener.Dispose(); + public static void RemoveDummyListener() => Listener.Dispose(); public static Meter GetMeter(string name) => new(name, InstrumentationVersion.Value); diff --git a/src/Core/src/Eventuous.Persistence/ExpectedStreamVersion.cs b/src/Core/src/Eventuous.Persistence/ExpectedStreamVersion.cs index 1c33e2c72..8a824e7a1 100644 --- a/src/Core/src/Eventuous.Persistence/ExpectedStreamVersion.cs +++ b/src/Core/src/Eventuous.Persistence/ExpectedStreamVersion.cs @@ -11,10 +11,10 @@ public readonly record struct ExpectedStreamVersion(long Value) { } public record struct StreamReadPosition { - public StreamReadPosition(long Value) { - if (Value < 0) throw new ArgumentOutOfRangeException(nameof(Value), "StreamReadPosition cannot be negative."); + public StreamReadPosition(long value) { + if (value < 0) throw new ArgumentOutOfRangeException(nameof(value), "StreamReadPosition cannot be negative."); - this.Value = Value; + Value = value; } public static readonly StreamReadPosition Start = new(0L); diff --git a/src/Core/src/Eventuous.Serialization/EventSerializer.cs b/src/Core/src/Eventuous.Serialization/EventSerializer.cs index 7d33f9c86..5f98d065e 100644 --- a/src/Core/src/Eventuous.Serialization/EventSerializer.cs +++ b/src/Core/src/Eventuous.Serialization/EventSerializer.cs @@ -10,12 +10,12 @@ namespace Eventuous; /// [PublicAPI] public static class EventSerializer { - static IEventSerializer? _default; + static IEventSerializer? defaultSerializer; /// /// Gets the default event serializer. Throws if not configured. /// - public static IEventSerializer Default => _default + public static IEventSerializer Default => defaultSerializer ?? throw new InvalidOperationException( "No default event serializer configured. " + "Call EventSerializer.SetDefault() at startup or register IEventSerializer in DI. " @@ -25,7 +25,7 @@ public static class EventSerializer { /// Sets the default event serializer instance. /// public static void SetDefault(IEventSerializer serializer) - => _default = serializer ?? throw new ArgumentNullException(nameof(serializer)); + => defaultSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); /// /// Sets the default event serializer only if one has not already been configured. @@ -34,6 +34,6 @@ public static void SetDefault(IEventSerializer serializer) public static bool TrySetDefault(IEventSerializer serializer) { ArgumentNullException.ThrowIfNull(serializer); - return Interlocked.CompareExchange(ref _default, serializer, null) == null; + return Interlocked.CompareExchange(ref defaultSerializer, serializer, null) == null; } } diff --git a/src/Core/src/Eventuous.Shared/TypeMap/TypeMap.cs b/src/Core/src/Eventuous.Shared/TypeMap/TypeMap.cs index 2b051964f..e973af2de 100644 --- a/src/Core/src/Eventuous.Shared/TypeMap/TypeMap.cs +++ b/src/Core/src/Eventuous.Shared/TypeMap/TypeMap.cs @@ -46,23 +46,22 @@ struct MapEntry { public TV? Value { get; set; } } - // ReSharper disable once InconsistentNaming // ReSharper disable once StaticMemberInGenericType - static volatile int _index = -1; + static volatile int index = -1; const int DefaultCapacity = 10; // ReSharper disable once UnusedTypeParameter static class TypeSlot { // ReSharper disable once StaticMemberInGenericType - internal static readonly int Index = Interlocked.Increment(ref _index); + internal static readonly int Index = Interlocked.Increment(ref index); } static int GetIndex() => TypeSlot.Index; static int RecommendedCapacity { get { - var capacity = _index + 1; + var capacity = index + 1; if (capacity < DefaultCapacity) { capacity = DefaultCapacity; From d1e5eb926295fc9d161801b2a8663a89900ef6a8 Mon Sep 17 00:00:00 2001 From: Neal Mummau Date: Mon, 19 Oct 2026 21:42:06 -0400 Subject: [PATCH 3/6] fix(tests): remove unnecessary using directives issue #624 --- .../Subscriptions/GapAgeReleaseWhileRunningTest.cs | 1 - .../Subscriptions/GapRemediationBeforeSkipTest.cs | 1 - .../Eventuous.Tests.Postgres/Subscriptions/GapSkipTimeoutTest.cs | 1 - src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/MetricsFixture.cs | 1 - 4 files changed, 4 deletions(-) diff --git a/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapAgeReleaseWhileRunningTest.cs b/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapAgeReleaseWhileRunningTest.cs index 31e65c536..0b1d9dd87 100644 --- a/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapAgeReleaseWhileRunningTest.cs +++ b/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapAgeReleaseWhileRunningTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using Eventuous.Postgresql.Subscriptions; -using Eventuous.Tests.Persistence.Base.Fixtures; using Eventuous.Tests.Subscriptions.Base; namespace Eventuous.Tests.Postgres.Subscriptions; diff --git a/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapRemediationBeforeSkipTest.cs b/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapRemediationBeforeSkipTest.cs index 45b69ca20..30eada74e 100644 --- a/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapRemediationBeforeSkipTest.cs +++ b/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapRemediationBeforeSkipTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using Eventuous.Postgresql.Subscriptions; -using Eventuous.Tests.Persistence.Base.Fixtures; using Eventuous.Tests.Subscriptions.Base; namespace Eventuous.Tests.Postgres.Subscriptions; diff --git a/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapSkipTimeoutTest.cs b/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapSkipTimeoutTest.cs index 2cc308a88..eda09bfd3 100644 --- a/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapSkipTimeoutTest.cs +++ b/src/Postgres/test/Eventuous.Tests.Postgres/Subscriptions/GapSkipTimeoutTest.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. using Eventuous.Postgresql.Subscriptions; -using Eventuous.Tests.Persistence.Base.Fixtures; using Eventuous.Tests.Subscriptions.Base; namespace Eventuous.Tests.Postgres.Subscriptions; diff --git a/src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/MetricsFixture.cs b/src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/MetricsFixture.cs index e063132de..36f9a21eb 100644 --- a/src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/MetricsFixture.cs +++ b/src/Sqlite/test/Eventuous.Tests.Sqlite/Metrics/MetricsFixture.cs @@ -6,7 +6,6 @@ using Eventuous.Sqlite.Subscriptions; using Eventuous.Subscriptions.Checkpoints; using Eventuous.Subscriptions.Registrations; -using Eventuous.TestHelpers.TUnit.Logging; using Eventuous.Tests.OpenTelemetry.Fakes; using Eventuous.Tests.OpenTelemetry.Fixtures; using Eventuous.Tests.Sqlite.Fixtures; From 8ac367e0a4c7741adb69663d522aa80a7c88d0d3 Mon Sep 17 00:00:00 2001 From: Neal Mummau Date: Mon, 19 Oct 2026 21:42:45 -0400 Subject: [PATCH 4/6] fix(tests): compile the analyzer fixture as test data Exclude Analyzed.cs from project compilation while preserving its explicit imports for standalone Roslyn analysis. Reference required assemblies explicitly and fail tests on compiler errors or unexpected analyzer diagnostics. Allow compiler errors only in the intentional missing-assembly test. issue #624 --- .../Analyzer_Ev001_Tests.cs | 30 +++++++++---------- .../Eventuous.Tests.Shared.Analyzers.csproj | 5 ++++ 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/Core/test/Eventuous.Tests.Shared.Analyzers/Analyzer_Ev001_Tests.cs b/src/Core/test/Eventuous.Tests.Shared.Analyzers/Analyzer_Ev001_Tests.cs index 3c547b6d8..17fb5221e 100644 --- a/src/Core/test/Eventuous.Tests.Shared.Analyzers/Analyzer_Ev001_Tests.cs +++ b/src/Core/test/Eventuous.Tests.Shared.Analyzers/Analyzer_Ev001_Tests.cs @@ -112,7 +112,7 @@ public async Task Should_stay_silent_when_event_type_attribute_cannot_be_resolve var compilation = CreateCompilation(source, includeSharedAssembly: false); var analyzer = new EventUsageAnalyzer(); - var diagnostics = await GetAnalyzerDiagnosticsAsync(compilation, analyzer); + var diagnostics = await GetAnalyzerDiagnosticsAsync(compilation, analyzer, allowCompilationErrors: true); await Assert.That(diagnostics.Length).IsEqualTo(0); } @@ -128,11 +128,18 @@ public async Task Should_resolve_all_well_known_type_names() { await Assert.That(unresolved).IsEmpty(); } - static async Task GetAnalyzerDiagnosticsAsync(Compilation compilation, EventUsageAnalyzer analyzer) { + static async Task GetAnalyzerDiagnosticsAsync(Compilation compilation, EventUsageAnalyzer analyzer, bool allowCompilationErrors = false) { + if (!allowCompilationErrors) { + var errors = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToArray(); + await Assert.That(errors).IsEmpty(); + } + var withAnalyzers = compilation.WithAnalyzers([analyzer]); var diagnostics = await withAnalyzers.GetAnalyzerDiagnosticsAsync().ConfigureAwait(false); - // Filter out anything not from our analyzer id just in case - return [.. diagnostics.Where(d => d.Id == EventUsageAnalyzer.DiagnosticId)]; + + // Fail on unexpected diagnostics, including analyzer failures such as AD0001. + await Assert.That(diagnostics.Where(d => d.Id != EventUsageAnalyzer.DiagnosticId)).IsEmpty(); + return [.. diagnostics]; } static string LoadAnalyzedSource([CallerFilePath] string? caller = null) { @@ -148,7 +155,8 @@ static CSharpCompilation CreateCompilation(string source, bool includeSharedAsse MetadataReference.CreateFromFile(typeof(object).GetTypeInfo().Assembly.Location), MetadataReference.CreateFromFile(typeof(Enumerable).GetTypeInfo().Assembly.Location), MetadataReference.CreateFromFile(typeof(State<>).Assembly.Location), - MetadataReference.CreateFromFile(typeof(Aggregate<>).Assembly.Location) + MetadataReference.CreateFromFile(typeof(Aggregate<>).Assembly.Location), + MetadataReference.CreateFromFile(typeof(JetBrains.Annotations.UsedImplicitlyAttribute).Assembly.Location) }; if (includeSharedAssembly) { @@ -162,7 +170,7 @@ static CSharpCompilation CreateCompilation(string source, bool includeSharedAsse var runtimeDir = Path.GetDirectoryName(typeof(object).Assembly.Location)!; refs.AddRange(Directory.GetFiles(runtimeDir, "System.*.dll").Select(dll => MetadataReference.CreateFromFile(dll)).Cast()); - TryAddRef(refs, "netstandard"); + refs.Add(MetadataReference.CreateFromFile(Path.Combine(runtimeDir, "netstandard.dll"))); var compilation = CSharpCompilation.Create( assemblyName: "Analyzer_Ev001_Tests_Assembly", @@ -176,14 +184,4 @@ static CSharpCompilation CreateCompilation(string source, bool includeSharedAsse return compilation; } - - static void TryAddRef(List refs, string asmName) { - try { - var asm = AppDomain.CurrentDomain.GetAssemblies().FirstOrDefault(a => a.GetName().Name == asmName); - if (asm != null) refs.Add(MetadataReference.CreateFromFile(asm.Location)); - } - catch { - // ignore - } - } } diff --git a/src/Core/test/Eventuous.Tests.Shared.Analyzers/Eventuous.Tests.Shared.Analyzers.csproj b/src/Core/test/Eventuous.Tests.Shared.Analyzers/Eventuous.Tests.Shared.Analyzers.csproj index 4df435ab6..d0d703750 100644 --- a/src/Core/test/Eventuous.Tests.Shared.Analyzers/Eventuous.Tests.Shared.Analyzers.csproj +++ b/src/Core/test/Eventuous.Tests.Shared.Analyzers/Eventuous.Tests.Shared.Analyzers.csproj @@ -4,6 +4,11 @@ Exe true + + + + + From 5f41583b6a1d9a4b3325eb12ea5d592bba02309c Mon Sep 17 00:00:00 2001 From: Neal Mummau Date: Mon, 19 Oct 2026 22:10:25 -0400 Subject: [PATCH 5/6] ci(mongo): pin integration test runner to Ubuntu 24.04 Use a per-suite runner override to avoid MongoDB startup failures on Ubuntu 26.04 kernels. Document the upstream compatibility issue. Fixes #626 --- .github/workflows/tests.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 294883a59..e67119c72 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,7 +79,7 @@ jobs: # # When adding a new integration test project (one that uses Testcontainers), add a suite entry. name: "Build and test ${{ matrix.suite.name }}" - runs-on: ubuntu-latest + runs-on: ${{ matrix.suite.runner || 'ubuntu-latest' }} strategy: fail-fast: false max-parallel: 4 @@ -93,6 +93,9 @@ jobs: project: src/SqlServer/test/Eventuous.Tests.SqlServer/Eventuous.Tests.SqlServer.csproj - name: mongo project: src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Eventuous.Tests.Projections.MongoDB.csproj + # Avoid MongoDB's startup check failure on Ubuntu 26.04 kernels. + # https://jira.mongodb.org/browse/SERVER-136848 + runner: ubuntu-24.04 - name: kafka project: src/Kafka/test/Eventuous.Tests.Kafka/Eventuous.Tests.Kafka.csproj - name: rabbitmq From 32d246751b61588ad73f417b4b98a315335e3593 Mon Sep 17 00:00:00 2001 From: Neal Mummau Date: Mon, 19 Oct 2026 22:21:38 -0400 Subject: [PATCH 6/6] fix(ci): use MongoDB 7 for projection integration tests MongoDB 8 exits on affected Linux 6.19+ kernels, including the runner used after pinning Ubuntu 24.04. Use the unaffected MongoDB 7.0 test image and remove the ineffective runner override. Validation: all nine MongoDB integration tests passed on net10.0 in Debug CI; targeted style verification and workflow YAML validation passed. --- .github/workflows/tests.yml | 5 +---- .../Fixtures/IntegrationFixture.cs | 4 +++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e67119c72..294883a59 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -79,7 +79,7 @@ jobs: # # When adding a new integration test project (one that uses Testcontainers), add a suite entry. name: "Build and test ${{ matrix.suite.name }}" - runs-on: ${{ matrix.suite.runner || 'ubuntu-latest' }} + runs-on: ubuntu-latest strategy: fail-fast: false max-parallel: 4 @@ -93,9 +93,6 @@ jobs: project: src/SqlServer/test/Eventuous.Tests.SqlServer/Eventuous.Tests.SqlServer.csproj - name: mongo project: src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Eventuous.Tests.Projections.MongoDB.csproj - # Avoid MongoDB's startup check failure on Ubuntu 26.04 kernels. - # https://jira.mongodb.org/browse/SERVER-136848 - runner: ubuntu-24.04 - name: kafka project: src/Kafka/test/Eventuous.Tests.Kafka/Eventuous.Tests.Kafka.csproj - name: rabbitmq diff --git a/src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Fixtures/IntegrationFixture.cs b/src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Fixtures/IntegrationFixture.cs index 0c8f0e550..c99a56dd3 100644 --- a/src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Fixtures/IntegrationFixture.cs +++ b/src/Mongo/test/Eventuous.Tests.Projections.MongoDB/Fixtures/IntegrationFixture.cs @@ -42,7 +42,9 @@ public async Task InitializeAsync() { var settings = KurrentDBClientSettings.Create(_esdbContainer.GetConnectionString()); Client = new(settings); EventStore = new KurrentDBEventStore(Client); - _mongoContainer = new MongoDbBuilder().WithImage("mongo:8").Build(); + // MongoDB 8 cannot start on affected Linux 6.19+ kernels used by CI runners. + // MongoDB 7.0 is unaffected: https://jira.mongodb.org/browse/SERVER-125742 + _mongoContainer = new MongoDbBuilder().WithImage("mongo:7.0").Build(); await _mongoContainer.StartAsync(); var mongoSettings = MongoClientSettings.FromConnectionString(_mongoContainer.GetConnectionString()); Mongo = new MongoClient(mongoSettings).GetDatabase("bookings");