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; }