diff --git a/.docfx/Dockerfile.docfx b/.docfx/Dockerfile.docfx
index ca80886..1719a33 100644
--- a/.docfx/Dockerfile.docfx
+++ b/.docfx/Dockerfile.docfx
@@ -1,4 +1,4 @@
-ARG NGINX_VERSION=1.30.0-alpine
+ARG NGINX_VERSION=1.31.0-alpine
FROM --platform=$BUILDPLATFORM nginx:${NGINX_VERSION} AS base
RUN rm -rf /usr/share/nginx/html/*
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index d014a76..4cd6ee6 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -180,6 +180,28 @@ Internal classes and methods must be validated by exercising the public API that
- Public entry points provide sufficient coverage of internal code paths.
- The internal implementation exists solely as a helper or utility for public-facing functionality.
+## 10. ExcludeFromCodeCoverage Prohibition
+
+**Do not use `ExcludeFromCodeCoverage` attribute on any code.** This includes:
+
+- Test classes or test methods
+- Production code
+- Configuration code
+- Any other code path
+
+### Rationale
+
+- Excluding code from coverage hides gaps and creates false confidence in test completeness.
+- If a code path cannot or should not be tested, refactor the code to eliminate that path rather than hiding it from metrics.
+- Every executable line should be covered by tests or be genuinely unreachable (dead code to be removed).
+
+### Alternative Approaches
+
+- **Untestable code paths**: Refactor to separate concerns and eliminate the untestable path.
+- **External dependencies**: Use test doubles (fakes, stubs, spies) instead of excluding from coverage.
+- **Configuration-only code**: Move to configuration files or extract into testable methods.
+- **Generated or third-party code**: These should not be in the primary codebase; use NuGet packages or dedicated vendor folders if necessary.
+
---
description: 'Writing Performance Tests'
applyTo: "tuning/**, **/*Benchmark*.cs"
diff --git a/.github/workflows/ci-pipeline.yml b/.github/workflows/ci-pipeline.yml
index 1dc8b95..8aadd65 100644
--- a/.github/workflows/ci-pipeline.yml
+++ b/.github/workflows/ci-pipeline.yml
@@ -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
@@ -21,6 +25,7 @@ jobs:
name: initialize
runs-on: ubuntu-24.04
outputs:
+ run-mac-tests: ${{ steps.vars.outputs.run-mac-tests }}
run-privileged-jobs: ${{ steps.vars.outputs.run-privileged-jobs }}
strong-name-key-filename: ${{ steps.vars.outputs.strong-name-key-filename }}
build-switches: ${{ steps.vars.outputs.build-switches }}
@@ -29,6 +34,12 @@ jobs:
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"
@@ -101,10 +112,72 @@ jobs:
build: true # required for xunitv3
download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }}
+ test_mac:
+ if: ${{ needs.init.outputs.run-mac-tests == 'true' }}
+ name: call-test-mac
+ needs: [init, build]
+ strategy:
+ fail-fast: false
+ matrix:
+ arch: [X64, ARM64]
+ configuration: [Debug, Release]
+ uses: codebeltnet/jobs-dotnet-test/.github/workflows/default.yml@v3
+ with:
+ runs-on: ${{ matrix.arch == 'ARM64' && 'macos-26' || 'macos-26-intel' }}
+ configuration: ${{ matrix.configuration }}
+ build-switches: -p:SkipSignAssembly=true
+ restore: true
+ build: true # required for xunitv3
+ download-pattern: build-${{ matrix.configuration }}-${{ matrix.arch }}
+
+ test_qualitygate:
+ if: ${{ always() }}
+ name: test-qualitygate
+ needs: [init, test_linux, test_windows, test_mac]
+ runs-on: ubuntu-24.04
+ steps:
+ - name: Evaluate test results
+ shell: bash
+ env:
+ RUN_MAC_TESTS: ${{ needs.init.outputs.run-mac-tests }}
+ TEST_LINUX_RESULT: ${{ needs.test_linux.result }}
+ TEST_WINDOWS_RESULT: ${{ needs.test_windows.result }}
+ TEST_MAC_RESULT: ${{ needs.test_mac.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"
+
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]
+ needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-sonarcloud/.github/workflows/default.yml@v3
with:
organization: geekle
@@ -113,18 +186,18 @@ 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]
+ needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-codecov/.github/workflows/default.yml@v1
with:
repository: codebeltnet/shared-kernel
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]
+ needs: [init, build, test_qualitygate]
uses: codebeltnet/jobs-codeql/.github/workflows/default.yml@v3
permissions:
security-events: write
@@ -132,7 +205,7 @@ jobs:
deploy:
if: github.event_name != 'pull_request'
name: call-nuget
- needs: [build, pack, test_linux, test_windows, 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 }}
diff --git a/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt b/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt
index 63eb0fb..b9766b8 100644
--- a/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt
+++ b/.nuget/Codebelt.SharedKernel/PackageReleaseNotes.txt
@@ -1,3 +1,9 @@
+Version: 0.5.7
+Availability: .NET 10 and .NET 9
+
+# ALM
+- CHANGED Dependencies have been upgraded to the latest compatible versions for all supported target frameworks (TFMs)
+
Version: 0.5.6
Availability: .NET 10 and .NET 9
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8129f78..fdbb8de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,22 @@ 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.
+## [0.5.7] - 2026-05-27
+
+This is a patch release that improves test coverage, infrastructure resilience, and code quality through CI/CD enhancements, package updates, and developer guidance.
+
+### Added
+
+- Optional macOS test matrix in `ci-pipeline.yml` with conditional job gating, enabling ARM64 and X64 architecture testing when explicitly triggered.
+
+### Changed
+
+- `Microsoft.NET.Test.Sdk` upgraded to 18.6.0,
+- `coverlet.collector` and `coverlet.msbuild` upgraded to 10.0.1,
+- `.github/copilot-instructions.md` extended with comprehensive code coverage prohibition guidance and refactoring best practices,
+- `ci-pipeline.yml` enhanced with test result evaluation quality gate and improved platform-specific test orchestration,
+- Test coverage expanded across `TokenTest.cs`, `TimeToLiveTest.cs`, `CorrelationIdTest.cs`, `ClockSkewTest.cs`, and `SecretTest.cs` with additional test cases.
+
## [0.5.6] - 2026-04-18
This is a service update that focuses on package dependencies.
@@ -103,6 +119,7 @@ Purely an ALM release. No changes to the codebase.
- CoordinatedUniversalTime record in the Codebelt.SharedKernel namespace that represents an object that can be used when you need a timestamp that is based on an absolute time (UTC)
- TimeToLive record in the Codebelt.SharedKernel namespace that represents an object that can be used when issuing authentication tokens or similar (TTL)
+[0.5.7]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.6...v0.5.7
[0.5.6]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.5...v0.5.6
[0.5.5]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.4...v0.5.5
[0.5.4]: https://github.com/codebeltnet/shared-kernel/compare/v0.5.3...v0.5.4
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 098d261..9b39265 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,15 +3,15 @@
true
-
-
-
+
+
+
-
-
-
-
-
+
+
+
+
+
diff --git a/test/Codebelt.SharedKernel.Tests/ClockSkewTest.cs b/test/Codebelt.SharedKernel.Tests/ClockSkewTest.cs
index 77c5b3b..111e97d 100644
--- a/test/Codebelt.SharedKernel.Tests/ClockSkewTest.cs
+++ b/test/Codebelt.SharedKernel.Tests/ClockSkewTest.cs
@@ -77,6 +77,16 @@ public void FromSeconds_ShouldHaveThirtySecondsSkew()
Assert.Equal(30, sut.Value.TotalSeconds);
}
+ [Fact]
+ public void ConversionAndStringRepresentation_ShouldRepresentCorrectly()
+ {
+ ClockSkew sut = TimeSpan.FromSeconds(30);
+ TimeSpan actual = sut;
+
+ Assert.Equal(TimeSpan.FromSeconds(30), actual);
+ Assert.Equal("00:00:30", sut.ToString());
+ }
+
[Fact]
public void Marshalling_ShouldRepresentCorrectly()
{
diff --git a/test/Codebelt.SharedKernel.Tests/CorrelationIdTest.cs b/test/Codebelt.SharedKernel.Tests/CorrelationIdTest.cs
index aa32d17..4f8e5c1 100644
--- a/test/Codebelt.SharedKernel.Tests/CorrelationIdTest.cs
+++ b/test/Codebelt.SharedKernel.Tests/CorrelationIdTest.cs
@@ -100,6 +100,17 @@ public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenValueHasConsi
Assert.Equal("0", sut.ActualValue);
}
+ [Fact]
+ public void ConversionOperators_ShouldRepresentCorrectly()
+ {
+ var value = Guid.Parse("1acb4e49-28a6-4206-b22b-2392ffd4e605");
+ CorrelationId fromGuid = value;
+ CorrelationId fromString = fromGuid.Value;
+
+ Assert.Equal("1acb4e4928a64206b22b2392ffd4e605", fromGuid.Value);
+ Assert.Equal(fromGuid, fromString);
+ }
+
[Fact]
public void Marshalling_ShouldRepresentCorrectly()
{
diff --git a/test/Codebelt.SharedKernel.Tests/Security/SecretTest.cs b/test/Codebelt.SharedKernel.Tests/Security/SecretTest.cs
index 9a3e4e2..7909568 100644
--- a/test/Codebelt.SharedKernel.Tests/Security/SecretTest.cs
+++ b/test/Codebelt.SharedKernel.Tests/Security/SecretTest.cs
@@ -1,5 +1,6 @@
using System;
using System.Linq;
+using System.Text;
using Cuemon;
using Cuemon.Extensions.IO;
using Codebelt.Extensions.Xunit;
@@ -98,6 +99,15 @@ public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenValueHasExcee
Assert.Equal("129 > 128", sut.ActualValue);
}
+ [Fact]
+ public void StringAndByteRepresentation_ShouldRepresentCorrectly()
+ {
+ var sut = new Secret("1acb4e4928a64206b22b2392ffd4e605");
+
+ Assert.Equal("1acb4e4928a64206b22b2392ffd4e605", sut.ToString());
+ Assert.Equal(Encoding.UTF8.GetBytes(sut.Value), sut.ToByteArray());
+ }
+
[Fact]
public void Marshalling_ShouldRepresentCorrectly()
{
diff --git a/test/Codebelt.SharedKernel.Tests/TimeToLiveTest.cs b/test/Codebelt.SharedKernel.Tests/TimeToLiveTest.cs
index 27971bd..a747885 100644
--- a/test/Codebelt.SharedKernel.Tests/TimeToLiveTest.cs
+++ b/test/Codebelt.SharedKernel.Tests/TimeToLiveTest.cs
@@ -101,6 +101,16 @@ public void FromMinutes_ShouldHaveFifteenMinutesLifespan()
Assert.Equal(15, sut.Value.TotalMinutes);
}
+ [Fact]
+ public void ConversionAndStringRepresentation_ShouldRepresentCorrectly()
+ {
+ TimeToLive sut = TimeSpan.FromMinutes(15);
+ TimeSpan actual = sut;
+
+ Assert.Equal(TimeSpan.FromMinutes(15), actual);
+ Assert.Equal("00:15:00", sut.ToString());
+ }
+
[Fact]
public void Marshalling_ShouldRepresentCorrectly()
{
diff --git a/test/Codebelt.SharedKernel.Tests/TokenTest.cs b/test/Codebelt.SharedKernel.Tests/TokenTest.cs
index 3372637..dc18989 100644
--- a/test/Codebelt.SharedKernel.Tests/TokenTest.cs
+++ b/test/Codebelt.SharedKernel.Tests/TokenTest.cs
@@ -105,6 +105,16 @@ public void Constructor_ShouldNotThrowArgumentOutOfRangeException_WhenValueHasEx
Assert.Equal(255, sut.Value.Length);
}
+ [Fact]
+ public void Constructor_ShouldNotThrowArgumentOutOfRangeException_WhenMaximumCharacterFrequencyIsDisabled()
+ {
+ var sut = new Token(new string('a', 40), o => o.MaximumCharacterFrequency = 0);
+
+ TestOutput.WriteLine(sut);
+
+ Assert.Equal(40, sut.Value.Length);
+ }
+
[Fact]
public void Constructor_ShouldThrowArgumentOutOfRangeException_WhenValueHasHighCharacterFrequency()
{