diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..1feb07b
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,279 @@
+name: CI
+
+on:
+ push:
+ branches: [master]
+ pull_request:
+ branches: [master]
+
+env:
+ MAJOR_MINOR: '1.15'
+
+permissions:
+ contents: write
+ pull-requests: write
+ security-events: write
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+ 10.0.x
+
+ - name: Restore
+ run: dotnet restore
+
+ - name: Build
+ run: dotnet build -c Release --no-restore 2>&1 | tee build.log
+
+ - name: Check warnings
+ run: |
+ WARNING_COUNT=$(grep -c " warning " build.log || true)
+ echo "Build warnings: $WARNING_COUNT"
+ if [ "$WARNING_COUNT" -gt 15 ]; then
+ echo "::error::Build has $WARNING_COUNT warnings (threshold: 15)"
+ exit 1
+ fi
+
+ - name: Test with coverage
+ run: dotnet test -c Release --no-build --verbosity normal --collect:"XPlat Code Coverage" --results-directory ./coverage
+
+ - name: Upload coverage to Codecov
+ uses: codecov/codecov-action@v5
+ with:
+ directory: ./coverage
+ fail_ci_if_error: false
+ token: ${{ secrets.CODECOV_TOKEN }}
+
+ - name: Compute version
+ id: version
+ run: |
+ LATEST_TAG=$(git tag -l "${MAJOR_MINOR}.*" --sort=-v:refname \
+ | grep -E "^${MAJOR_MINOR}\.[0-9]+$" \
+ | head -1)
+
+ if [ -z "$LATEST_TAG" ]; then
+ PATCH=0
+ else
+ PATCH=$(echo "$LATEST_TAG" | sed "s/${MAJOR_MINOR}\.\([0-9]*\)/\1/")
+ PATCH=$((PATCH + 1))
+ fi
+
+ VERSION="${MAJOR_MINOR}.${PATCH}"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "previous_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
+ echo "Computed version: $VERSION (previous: $LATEST_TAG)"
+
+ - name: Compute pre-release version
+ id: preversion
+ if: github.ref != 'refs/heads/master'
+ run: |
+ VERSION="${{ steps.version.outputs.version }}"
+ PRE_BASE="${VERSION}-pre"
+
+ LATEST_PRE=$(git tag -l "${PRE_BASE}.*" --sort=-v:refname | head -1)
+
+ if [ -z "$LATEST_PRE" ]; then
+ COUNTER=1
+ else
+ COUNTER=$(echo "$LATEST_PRE" | sed "s/.*-pre\.\([0-9]*\)/\1/")
+ COUNTER=$((COUNTER + 1))
+ fi
+
+ PRE_VERSION="${PRE_BASE}.${COUNTER}"
+ echo "version=$PRE_VERSION" >> "$GITHUB_OUTPUT"
+ echo "Computed pre-release version: $PRE_VERSION"
+
+ - name: Pack (stable — push to master)
+ if: github.ref == 'refs/heads/master' && github.event_name == 'push'
+ run: |
+ dotnet pack Tharga.Toolkit.Standard/Tharga.Toolkit.Standard.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.version }}
+ dotnet pack Tharga.Toolkit/Tharga.Toolkit.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.version.outputs.version }}
+
+ - name: Pack (pre-release — pull request)
+ if: github.event_name == 'pull_request'
+ run: |
+ dotnet pack Tharga.Toolkit.Standard/Tharga.Toolkit.Standard.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.preversion.outputs.version }}
+ dotnet pack Tharga.Toolkit/Tharga.Toolkit.csproj -c Release --no-build -o ./artifacts -p:PackageVersion=${{ steps.preversion.outputs.version }}
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: nuget-packages
+ path: ./artifacts/
+
+ security:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: csharp
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: |
+ 8.0.x
+ 9.0.x
+ 10.0.x
+
+ - name: Build for CodeQL
+ run: dotnet build -c Release
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+
+ release:
+ needs: [build, security]
+ runs-on: ubuntu-latest
+ if: github.ref == 'refs/heads/master' && github.event_name == 'push'
+ environment: release
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-packages
+ path: ./artifacts/
+
+ - name: Compute version
+ id: version
+ run: |
+ LATEST_TAG=$(git tag -l "${MAJOR_MINOR}.*" --sort=-v:refname \
+ | grep -E "^${MAJOR_MINOR}\.[0-9]+$" \
+ | head -1)
+
+ if [ -z "$LATEST_TAG" ]; then
+ PATCH=0
+ else
+ PATCH=$(echo "$LATEST_TAG" | sed "s/${MAJOR_MINOR}\.\([0-9]*\)/\1/")
+ PATCH=$((PATCH + 1))
+ fi
+
+ VERSION="${MAJOR_MINOR}.${PATCH}"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+ echo "previous_tag=$LATEST_TAG" >> "$GITHUB_OUTPUT"
+
+ - name: Push to NuGet
+ run: |
+ for pkg in ./artifacts/*.nupkg; do
+ echo "Pushing $pkg..."
+ dotnet nuget push "$pkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate || true
+ done
+
+ - name: Create GitHub Release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ NOTES_FLAG=""
+ if [ -n "${{ steps.version.outputs.previous_tag }}" ]; then
+ NOTES_FLAG="--notes-start-tag ${{ steps.version.outputs.previous_tag }}"
+ fi
+ gh release create "${{ steps.version.outputs.version }}" \
+ --title "v${{ steps.version.outputs.version }}" \
+ --generate-notes \
+ $NOTES_FLAG \
+ ./artifacts/*.nupkg
+
+ - name: Comment released version on merged PR
+ if: always()
+ continue-on-error: true
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ PR_NUMBER=$(echo "${{ github.event.head_commit.message }}" | grep -oP 'Merge pull request #\K\d+' | head -1)
+ if [ -n "$PR_NUMBER" ]; then
+ gh pr comment "$PR_NUMBER" --body "Released as **v${{ steps.version.outputs.version }}** — https://github.com/${{ github.repository }}/releases/tag/${{ steps.version.outputs.version }}"
+ else
+ echo "No PR number found in commit message — skipping PR comment."
+ fi
+
+ prerelease:
+ needs: [build, security]
+ runs-on: ubuntu-latest
+ if: github.ref != 'refs/heads/master' && github.event_name == 'pull_request'
+ environment: prerelease
+
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Setup .NET
+ uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: 10.0.x
+
+ - name: Download artifacts
+ uses: actions/download-artifact@v4
+ with:
+ name: nuget-packages
+ path: ./artifacts/
+
+ - name: Compute pre-release version
+ id: version
+ run: |
+ LATEST_TAG=$(git tag -l "${MAJOR_MINOR}.*" --sort=-v:refname \
+ | grep -E "^${MAJOR_MINOR}\.[0-9]+$" \
+ | head -1)
+
+ if [ -z "$LATEST_TAG" ]; then
+ PATCH=0
+ else
+ PATCH=$(echo "$LATEST_TAG" | sed "s/${MAJOR_MINOR}\.\([0-9]*\)/\1/")
+ PATCH=$((PATCH + 1))
+ fi
+
+ PRE_BASE="${MAJOR_MINOR}.${PATCH}-pre"
+ LATEST_PRE=$(git tag -l "${PRE_BASE}.*" --sort=-v:refname | head -1)
+
+ if [ -z "$LATEST_PRE" ]; then
+ COUNTER=1
+ else
+ COUNTER=$(echo "$LATEST_PRE" | sed "s/.*-pre\.\([0-9]*\)/\1/")
+ COUNTER=$((COUNTER + 1))
+ fi
+
+ VERSION="${PRE_BASE}.${COUNTER}"
+ echo "version=$VERSION" >> "$GITHUB_OUTPUT"
+
+ - name: Push to NuGet
+ run: |
+ for pkg in ./artifacts/*.nupkg; do
+ echo "Pushing $pkg..."
+ dotnet nuget push "$pkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate || true
+ done
+
+ - name: Create GitHub Pre-release
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh release create "${{ steps.version.outputs.version }}" \
+ --title "v${{ steps.version.outputs.version }} (pre-release)" \
+ --notes "Pre-release from \`${{ github.head_ref }}\` branch." \
+ --prerelease \
+ ./artifacts/*.nupkg
diff --git a/Tharga.Toolkit.Standard.Tests/Tharga.Toolkit.Standard.Tests.csproj b/Tharga.Toolkit.Standard.Tests/Tharga.Toolkit.Standard.Tests.csproj
index c7f3a99..cec1194 100644
--- a/Tharga.Toolkit.Standard.Tests/Tharga.Toolkit.Standard.Tests.csproj
+++ b/Tharga.Toolkit.Standard.Tests/Tharga.Toolkit.Standard.Tests.csproj
@@ -3,6 +3,7 @@
net10.0
false
+ $(NoWarn);xUnit1051
diff --git a/Tharga.Toolkit.Tests/DependencyTest.cs b/Tharga.Toolkit.Tests/DependencyTest.cs
index f98be65..a1253d4 100644
--- a/Tharga.Toolkit.Tests/DependencyTest.cs
+++ b/Tharga.Toolkit.Tests/DependencyTest.cs
@@ -19,6 +19,7 @@ public void Has_no_accidental_dependency()
//act
var dps = GetDependencies()
.Where(x => x.Name != "Microsoft.Extensions.DependencyInjection.Abstractions")
+ .Where(x => !x.Name.StartsWith("System."))
.ToArray();
//Assert
diff --git a/Tharga.Toolkit.Tests/Tharga.Toolkit.Tests.csproj b/Tharga.Toolkit.Tests/Tharga.Toolkit.Tests.csproj
index 5a431d0..68289ce 100644
--- a/Tharga.Toolkit.Tests/Tharga.Toolkit.Tests.csproj
+++ b/Tharga.Toolkit.Tests/Tharga.Toolkit.Tests.csproj
@@ -3,6 +3,7 @@
net10.0
false
+ $(NoWarn);xUnit1051
diff --git a/Tharga.Toolkit/HashString.cs b/Tharga.Toolkit/HashString.cs
index 42d8fd9..7fcf163 100644
--- a/Tharga.Toolkit/HashString.cs
+++ b/Tharga.Toolkit/HashString.cs
@@ -19,7 +19,7 @@ public HashString(string value, HashFormat format)
}
/// Gets the formatted hash string.
- public string Value { get; }
+ public new string Value { get; }
/// Gets the format used to encode this hash string.
public HashFormat Format { get; }