From a06c350f3119595a444d35d8bec782cea675a4fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Mon, 16 Dec 2024 09:44:20 +0100 Subject: [PATCH] refactor: add Nuke build (#688) Use [NUKE](https://nuke.build/) for the build pipeline. * Adjust build output * Update pipelines * Delete test-report.yml --- .../stryker/Stryker.Config.AccessControl.json | 19 - .../stryker/Stryker.Config.Compression.json | 19 - .github/stryker/Stryker.Config.Testing.json | 48 -- .github/stryker/Stryker.Config.json | 20 - .github/workflows/build.yml | 590 +++++++----------- .github/workflows/ci.yml | 422 ++++--------- .github/workflows/test-report.yml | 42 -- .gitignore | 5 +- .nuke/build.schema.json | 144 +++++ .nuke/parameters.json | 4 + Directory.Packages.props | 6 + Examples/Directory.Build.props | 1 - Pipeline/.editorconfig | 11 + Pipeline/Build.ApiChecks.cs | 27 + Pipeline/Build.CodeAnalysis.cs | 42 ++ Pipeline/Build.CodeCoverage.cs | 25 + Pipeline/Build.Compile.cs | 80 +++ Pipeline/Build.MutationTests.cs | 208 ++++++ Pipeline/Build.Pack.cs | 99 +++ Pipeline/Build.UnitTest.cs | 76 +++ Pipeline/Build.cs | 34 + Pipeline/Build.csproj | 38 ++ Pipeline/Build.csproj.DotSettings | 31 + Pipeline/BuildExtensions.cs | 36 ++ Pipeline/Configuration.cs | 16 + Pipeline/Directory.Build.props | 8 + Pipeline/Directory.Build.targets | 8 + Source/Directory.Build.props | 1 - Testably.Abstractions.sln | 20 +- Tests/Api/Directory.Build.props | 1 - .../Testably.Abstractions.Api.Tests/Helper.cs | 7 +- Tests/Directory.Build.props | 1 - .../Testably.Abstractions.TestHelpers.csproj | 4 - ....Abstractions.Tests.SourceGenerator.csproj | 1 - Tests/Settings/Directory.Build.props | 1 - build.cmd | 7 + build.ps1 | 74 +++ build.sh | 67 ++ 38 files changed, 1421 insertions(+), 822 deletions(-) delete mode 100644 .github/stryker/Stryker.Config.AccessControl.json delete mode 100644 .github/stryker/Stryker.Config.Compression.json delete mode 100644 .github/stryker/Stryker.Config.Testing.json delete mode 100644 .github/stryker/Stryker.Config.json delete mode 100644 .github/workflows/test-report.yml create mode 100644 .nuke/build.schema.json create mode 100644 .nuke/parameters.json create mode 100644 Pipeline/.editorconfig create mode 100644 Pipeline/Build.ApiChecks.cs create mode 100644 Pipeline/Build.CodeAnalysis.cs create mode 100644 Pipeline/Build.CodeCoverage.cs create mode 100644 Pipeline/Build.Compile.cs create mode 100644 Pipeline/Build.MutationTests.cs create mode 100644 Pipeline/Build.Pack.cs create mode 100644 Pipeline/Build.UnitTest.cs create mode 100644 Pipeline/Build.cs create mode 100644 Pipeline/Build.csproj create mode 100644 Pipeline/Build.csproj.DotSettings create mode 100644 Pipeline/BuildExtensions.cs create mode 100644 Pipeline/Configuration.cs create mode 100644 Pipeline/Directory.Build.props create mode 100644 Pipeline/Directory.Build.targets create mode 100755 build.cmd create mode 100644 build.ps1 create mode 100755 build.sh diff --git a/.github/stryker/Stryker.Config.AccessControl.json b/.github/stryker/Stryker.Config.AccessControl.json deleted file mode 100644 index bd9b0af05..000000000 --- a/.github/stryker/Stryker.Config.AccessControl.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "stryker-config": { - "project-info": { - "name": "github.com/Testably/Testably.Abstractions", - "module": "Testably.Abstractions.AccessControl" - }, - "test-projects": [ - "./Testably.Abstractions.AccessControl.Tests/Testably.Abstractions.AccessControl.Tests.csproj" - ], - "project": "Testably.Abstractions.AccessControl.csproj", - "target-framework": "net8.0", - "reporters": [ - "html", - "progress", - "cleartext" - ], - "mutation-level": "Advanced" - } -} diff --git a/.github/stryker/Stryker.Config.Compression.json b/.github/stryker/Stryker.Config.Compression.json deleted file mode 100644 index ca57fc3fc..000000000 --- a/.github/stryker/Stryker.Config.Compression.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "stryker-config": { - "project-info": { - "name": "github.com/Testably/Testably.Abstractions", - "module": "Testably.Abstractions.Compression" - }, - "test-projects": [ - "./Testably.Abstractions.Compression.Tests/Testably.Abstractions.Compression.Tests.csproj" - ], - "project": "Testably.Abstractions.Compression.csproj", - "target-framework": "net8.0", - "reporters": [ - "html", - "progress", - "cleartext" - ], - "mutation-level": "Advanced" - } -} diff --git a/.github/stryker/Stryker.Config.Testing.json b/.github/stryker/Stryker.Config.Testing.json deleted file mode 100644 index 7f8ae4ca1..000000000 --- a/.github/stryker/Stryker.Config.Testing.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "stryker-config": { - "project-info": { - "name": "github.com/Testably/Testably.Abstractions", - "module": "Testably.Abstractions.Testing" - }, - "test-projects": [ - "./Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj", - "./Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj" - ], - "project": "Testably.Abstractions.Testing.csproj", - "target-framework": "net8.0", - "reporters": [ - "html", - "progress", - "cleartext" - ], - "mutation-level": "Advanced", - "mutate": [ - // The enumeration options helper is a wrapper around Microsoft code - "!**/Testably.Abstractions.Testing/Helpers/EnumerationOptionsHelper.cs", - // The exception type is checked, but not the message, as this could be language dependent - "!**/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs", - // Indicates operating system specific code - "!**/Testably.Abstractions.Testing/Helpers/Execute.cs", - // The directory cleaner should cleanup the real file system - "!**/Testably.Abstractions.Testing/FileSystemInitializer/DirectoryCleaner.cs" - ], - "ignore-methods": [ - // The exception type is checked, but not the message, as this could be language dependent - "ExceptionFactory.*", - // Some checks are redundant but there for performance improvements - "InMemoryLocation.Equals", - // Indicates operating system specific code - "Testably.Abstractions.Testing.Helpers.Execute.*On*", - // Drives are not used in Linux - "ValidateDriveLetter", - // The encryption helper is only valid for testing purposes - "CreateDummyEncryptionAlgorithm", - // Triggered by invalid chars which don't exist in Linux - "ThrowCommonExceptionsIfPathIsInvalid", - // Calls to Thread.Sleep cannot be detected by a test - "System.Threading.Thread.Sleep", - // Ensures that an expectation from developers is met - "Debug.Assert" - ] - } -} diff --git a/.github/stryker/Stryker.Config.json b/.github/stryker/Stryker.Config.json deleted file mode 100644 index bf74eda69..000000000 --- a/.github/stryker/Stryker.Config.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "stryker-config": { - "project-info": { - "name": "github.com/Testably/Testably.Abstractions", - "module": "Testably.Abstractions" - }, - "test-projects": [ - "./Testably.Abstractions.Tests/Testably.Abstractions.Tests.csproj", - "./Testably.Abstractions.Testing.Tests/Testably.Abstractions.Testing.Tests.csproj" - ], - "project": "Testably.Abstractions.csproj", - "target-framework": "net8.0", - "reporters": [ - "html", - "progress", - "cleartext" - ], - "mutation-level": "Advanced" - } -} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6603201e0..aa1f8f706 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,373 +1,231 @@ -name: "Build" +name: Build on: - workflow_dispatch: - push: - branches: [main, 'release/v[0-9]+.[0-9]+.[0-9]+'] - -jobs: - test-macos: - name: Test (MacOS) - runs-on: macos-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Tag current commit - if: startsWith(github.ref, 'refs/heads/release/') - shell: bash - run: | - version="${GITHUB_REF#refs/heads/release/}" - git tag "${version}" - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build solution - run: dotnet build /p:NetCoreOnly=True --configuration "Release" - - name: Run tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: dotnet test --no-build --collect:"XPlat Code Coverage" - attempt_limit: 2 - - name: Upload coverage - uses: actions/upload-artifact@v3 - with: - name: Code coverage (MacOS) - path: "**/coverage.cobertura.xml" - - test-ubuntu: - name: Test (Ubuntu) - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Tag current commit - if: startsWith(github.ref, 'refs/heads/release/') - shell: bash - run: | - version="${GITHUB_REF#refs/heads/release/}" - git tag "${version}" - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build solution - run: dotnet build /p:NetCoreOnly=True --configuration "Release" - - name: Run tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: dotnet test --no-build --collect:"XPlat Code Coverage" - attempt_limit: 2 - - name: Upload coverage - uses: actions/upload-artifact@v3 - with: - name: Code coverage (Ubuntu) - path: "**/coverage.cobertura.xml" - - test-windows: - name: Test (Windows) - runs-on: windows-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Tag current commit - if: startsWith(github.ref, 'refs/heads/release/') - shell: bash - run: | - version="${GITHUB_REF#refs/heads/release/}" - git tag "${version}" - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build solution - run: dotnet build /p:NetCoreOnly=True --configuration "Release" - - name: Run tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: dotnet test --no-build --collect:"XPlat Code Coverage" - attempt_limit: 2 - - name: Upload coverage - uses: actions/upload-artifact@v3 - with: - name: Code coverage (Windows) - path: "**/coverage.cobertura.xml" - - test-net-framework: - name: Test (.NET Framework) - runs-on: windows-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Tag current commit - if: startsWith(github.ref, 'refs/heads/release/') - shell: bash - run: | - version="${GITHUB_REF#refs/heads/release/}" - git tag "${version}" - - name: Setup .NET - uses: actions/setup-dotnet@v4 - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v2 - - name: Setup VSTest - uses: darenm/Setup-VSTest@v1 - - name: Navigate to Workspace - run: cd $GITHUB_WORKSPACE - - name: Build solution - run: msbuild.exe Testably.Abstractions.sln /p:NetFrameworkOnly=True /p:platform="Any CPU" /p:configuration="Release" -t:restore,build -p:RestorePackagesConfig=true - - name: Run tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: vstest.console.exe .\Build\Tests\Testably.Abstractions.Tests\net48\Testably.Abstractions.Tests.dll .\Build\Tests\Testably.Abstractions.Parity.Tests\net48\Testably.Abstractions.Parity.Tests.dll .\Build\Tests\Testably.Abstractions.Testing.Tests\net48\Testably.Abstractions.Testing.Tests.dll /Logger:trx /ResultsDirectory:TestResults /collect:"Code Coverage;Format=Cobertura" - attempt_limit: 2 - - name: Upload coverage - uses: actions/upload-artifact@v3 - with: - name: Code coverage (.NET Framework) - path: "**/*.cobertura.xml" - - test-examples: - name: Test (Examples) - runs-on: windows-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Tag current commit - if: startsWith(github.ref, 'refs/heads/release/') - shell: bash - run: | - version="${GITHUB_REF#refs/heads/release/}" - git tag "${version}" - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build solution - run: dotnet build /p:NetCoreOnly=True --configuration "Release" - - name: Build example solution - run: dotnet build Examples /p:UseFileReferenceToTestablyLibraries=True - - name: Run example tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: dotnet test Examples --no-build - attempt_limit: 2 - - upload-coverage: - name: Upload coverage to Codacy - needs: [test-macos, test-ubuntu, test-windows, test-net-framework] - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - - name: Download code coverage files (MacOS) - uses: actions/download-artifact@v3 - with: - name: Code coverage (MacOS) - path: Coverage/MacOS - - name: Download code coverage files (Ubuntu) - uses: actions/download-artifact@v3 - with: - name: Code coverage (Ubuntu) - path: Coverage/Ubuntu - - name: Download code coverage files (Windows) - uses: actions/download-artifact@v3 - with: - name: Code coverage (Windows) - path: Coverage/Windows - - name: Generate coverage report - uses: danielpalme/ReportGenerator-GitHub-Action@v5.4.1 - with: - reports: "Coverage/**/*.cobertura.xml" - targetdir: "coverage-report" - reporttypes: "Cobertura" - - name: Publish coverage report to Codacy - uses: codacy/codacy-coverage-reporter-action@master - with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: coverage-report/Cobertura.xml + push: + branches: [ main ] + tags: [ 'v[0-9]+.[0-9]+.[0-9]+' ] - stryker-ubuntu: - name: Stryker mutation testing (Ubuntu) - runs-on: ubuntu-latest - timeout-minutes: 300 - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Install .NET Stryker - shell: bash - run: | - dotnet tool install dotnet-stryker --tool-path ../tools - - name: Analyze Testably.Abstractions.Testing +jobs: + unit-tests: + name: "Unit tests" + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Run unit tests (windows) + if: matrix.os == 'windows-latest' + run: ./build.ps1 CodeCoverage + - name: Run unit tests (ubuntu|macos) + if: matrix.os != 'windows-latest' + run: ./build.sh CodeCoverage + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-artifacts + path: | + ./Artifacts/* + ./TestResults/*.trx + + api-tests: + name: "API tests" + runs-on: ubuntu-latest env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - shell: bash - run: | - cd Tests - ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.Testing.json -v "${GITHUB_REF#refs/heads/}" -r "Dashboard" -r "cleartext" - - stryker-windows: - name: Stryker mutation testing (Windows) - runs-on: windows-latest - timeout-minutes: 300 - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Install .NET Stryker - shell: bash - run: | - dotnet tool install dotnet-stryker --tool-path ../tools - - name: Analyze Testably.Abstractions + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: API checks + run: ./build.sh ApiChecks + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: API-tests + path: | + ./Artifacts/* + ./TestResults/*.trx + + mutation-tests-linux: + name: "Mutation tests (Linux)" + if: ${{ github.actor != 'dependabot[bot]' }} + runs-on: ubuntu-latest env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - shell: bash - run: | - cd Tests - ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.json -v "${GITHUB_REF#refs/heads/}" -r "Dashboard" -r "cleartext" - - name: Analyze Testably.Abstractions.AccessControl + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Run mutation tests + run: ./build.sh MutationTestsLinux + + mutation-tests-windows: + name: "Mutation tests (Windows)" + if: ${{ github.actor != 'dependabot[bot]' }} + runs-on: windows-latest env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - shell: bash - run: | - cd Tests - ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.AccessControl.json -v "${GITHUB_REF#refs/heads/}" -r "Dashboard" -r "cleartext" - - name: Analyze Testably.Abstractions.Compression + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Run mutation tests + run: ./build.ps1 MutationTestsWindows + + static-code-analysis: + name: "Static code analysis" + runs-on: ubuntu-latest env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - shell: bash - run: | - cd Tests - ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.Compression.json -v "${GITHUB_REF#refs/heads/}" -r "Dashboard" -r "cleartext" - - deploy: - name: Deploy - if: startsWith(github.ref, 'refs/heads/release/') - runs-on: ubuntu-latest - environment: production - needs: [test-macos, test-ubuntu, test-windows, test-net-framework, test-examples, stryker-windows, stryker-ubuntu] - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Tag current commit - id: tag - shell: bash - run: | - version="${GITHUB_REF#refs/heads/release/}" - git tag "${version}" - git push origin "${version}" - echo "release_version=${version}" >> "$GITHUB_OUTPUT" - - name: Setup NuGet - uses: NuGet/setup-nuget@v2.0.1 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Prepare README.md - shell: bash - run: | - version="${GITHUB_REF#refs/heads/release/}" - # Add changelog badge to README.md - sed -i -e "2 a\[!\[Changelog](https:\/\/img\.shields\.io\/badge\/Changelog-${version}-blue)](https:\/\/github\.com\/Testably\/Testably\.Abstractions\/releases\/tag\/${version})" "./README.md" - for f in "README.md" "Docs/AccessControl.md" "Docs/Compression.md" "Docs/Interface.md" "Docs/Testing.md" - do - echo "Processing $f" # always double quote "$f" filename - # do something on $f - # Remove the codacy and sonarcloud badge as it is not aligned to the release - grep -v "app.codacy.com" "./$f" > "./$f.backup" && mv "./$f.backup" "./$f" - grep -v "sonarcloud.io" "./$f" > "./$f.backup" && mv "./$f.backup" "./$f" - # Change status badges to display explicit version - sed -i -e "s/branch=main/branch=release%2F${version}/g" "./$f" - sed -i -e "s/Testably.Abstractions%2Fmain/Testably.Abstractions%2Frelease%2F${version}/g" "./$f" - sed -i -e "s/Testably.Abstractions%2Fmain/Testably.Abstractions%2Frelease%2F${version}/g" "./$f" - sed -i -e "s/Testably.Abstractions\/main)/Testably.Abstractions\/release\/${version})/g" "./$f" - sed -i -e "s/Testably.Abstractions\/actions\/workflows\/build.yml\/badge.svg)/Testably.Abstractions\/actions\/workflows\/build.yml\/badge.svg?branch=release\/${version})/g" "./$f" - sed -i -e "s/Testably.Abstractions\/actions\/workflows\/build.yml)/Testably.Abstractions\/actions\/workflows\/build.yml?query=branch%3Arelease%2F${version})/g" "./$f" - # Add absolute path to example section - sed -i -e 's/\(Examples\/README.md\)/https:\/\/github.com\/Testably\/Testably.Abstractions\/blob\/main\/Examples\/README.md/g' "./$f" - done - - name: Build - run: dotnet build --configuration "Release" - - name: Publish - run: nuget push **\*.nupkg -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} - - name: Create GitHub release - uses: softprops/action-gh-release@v2 - with: - name: ${{ steps.tag.outputs.release_version }} - tag_name: ${{ steps.tag.outputs.release_version }} - token: ${{ secrets.GITHUB_TOKEN }} - generate_release_notes: true - - cleanup: - name: Cleanup - if: startsWith(github.ref, 'refs/heads/release/') - runs-on: ubuntu-latest - needs: [deploy] - steps: - - name: Comment relevant issues and pull requests - uses: apexskier/github-release-commenter@v1.3.6 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - comment-template: | - This is addressed in release {release_link}. - label-template: | - state: released - skip-label: | - state: released - dependencies - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Delete release branch - shell: bash - run: | - version="${GITHUB_REF#refs/heads/release/}" - git push origin -d "refs/heads/release/${version}" + REPORTGENERATOR_LICENSE: ${{ secrets.REPORTGENERATOR_LICENSE }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Run sonarcloud analysis + run: ./build.sh CodeAnalysis + + publish-test-results: + name: "Publish Tests Results" + needs: [ api-tests, unit-tests ] + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + if: always() + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + comment_mode: always + files: "artifacts/**/**/*.trx" + + pack: + name: "Pack" + runs-on: ubuntu-latest + needs: [ publish-test-results, mutation-tests-linux, mutation-tests-windows, static-code-analysis ] + env: + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Pack nuget packages + run: ./build.sh Pack + - name: Upload packages + if: always() + uses: actions/upload-artifact@v4 + with: + path: Artifacts/Packages + name: Packages + + push: + name: "Push" + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + runs-on: ubuntu-latest + environment: production + needs: [ pack ] + permissions: + contents: write + steps: + - name: Download packages + uses: actions/download-artifact@v4 + with: + name: Packages + path: Artifacts/Packages + - name: Publish + run: | + echo "Found the following packages to push:" + search_dir=Artifacts/Packages + for entry in Artifacts/Packages/*.nupkg + do + echo "- $entry" + done + for entry in Artifacts/Packages/*.nupkg + do + nuget push $entry -Source 'https://api.nuget.org/v3/index.json' -ApiKey ${{secrets.NUGET_API_KEY}} -SkipDuplicate + done + - name: Create GitHub release + continue-on-error: true + uses: softprops/action-gh-release@v2 + with: + name: ${{ steps.tag.outputs.release_version }} + tag_name: ${{ steps.tag.outputs.release_version }} + token: ${{ secrets.GITHUB_TOKEN }} + generate_release_notes: true + + finalize-release: + name: "Finalize release" + if: startsWith(github.ref, 'refs/tags/v') + runs-on: ubuntu-latest + needs: [ push ] + permissions: + contents: read + issues: write + pull-requests: write + steps: + - name: Comment relevant issues and pull requests + continue-on-error: true + uses: apexskier/github-release-commenter@v1.3.6 + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + comment-template: | + This is addressed in release {release_link}. + label-template: | + state: released + skip-label: | + state: released diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75049ec37..30c6b455b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,292 +6,144 @@ on: branches: [main] jobs: - test-macos: - name: Test (MacOS) - runs-on: macos-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build solution - run: dotnet build /p:NetCoreOnly=True --configuration "Release" - - name: Run tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: dotnet test --no-build --collect:"XPlat Code Coverage" --logger trx --results-directory "TestResults" - attempt_limit: 2 - - name: Upload test results (MacOS) - if: ${{ always() }} - uses: actions/upload-artifact@v3 - with: - name: Test results (MacOS) - path: TestResults - - name: Upload coverage - uses: actions/upload-artifact@v3 - with: - name: Code coverage (MacOS) - path: "**/coverage.cobertura.xml" - - test-ubuntu: - name: Test (Ubuntu) - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build solution - run: dotnet build /p:NetCoreOnly=True --configuration "Release" - - name: Run tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: dotnet test --no-build --collect:"XPlat Code Coverage" --logger trx --results-directory "TestResults" - attempt_limit: 2 - - name: Upload test results (Ubuntu) - if: ${{ always() }} - uses: actions/upload-artifact@v3 - with: - name: Test results (Ubuntu) - path: TestResults - - name: Upload coverage - uses: actions/upload-artifact@v3 - with: - name: Code coverage (Ubuntu) - path: "**/coverage.cobertura.xml" - - test-windows: - name: Test (Windows) - runs-on: windows-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build solution - run: dotnet build /p:NetCoreOnly=True --configuration "Release" - - name: Run tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: dotnet test --no-build --collect:"XPlat Code Coverage" --logger trx --results-directory "TestResults" - attempt_limit: 2 - - name: Upload test results (Windows) - if: ${{ always() }} - uses: actions/upload-artifact@v3 - with: - name: Test results (Windows) - path: TestResults - - name: Upload coverage - uses: actions/upload-artifact@v3 - with: - name: Code coverage (Windows) - path: "**/coverage.cobertura.xml" - - test-net-framework: - name: Test (.NET Framework) - runs-on: windows-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v2 - - name: Setup VSTest - uses: darenm/Setup-VSTest@v1 - - name: Navigate to Workspace - run: cd $GITHUB_WORKSPACE - - name: Build solution - run: msbuild.exe Testably.Abstractions.sln /p:NetFrameworkOnly=True /p:platform="Any CPU" /p:configuration="Release" -t:restore,build -p:RestorePackagesConfig=true - - name: Run tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: vstest.console.exe .\Build\Tests\Testably.Abstractions.Tests\net48\Testably.Abstractions.Tests.dll .\Build\Tests\Testably.Abstractions.Parity.Tests\net48\Testably.Abstractions.Parity.Tests.dll .\Build\Tests\Testably.Abstractions.Testing.Tests\net48\Testably.Abstractions.Testing.Tests.dll /Logger:trx /ResultsDirectory:TestResults /collect:"Code Coverage;Format=Cobertura" - attempt_limit: 2 - - name: Upload test results (.NET Framework) - if: ${{ always() }} - uses: actions/upload-artifact@v3 - with: - name: Test results (.NET Framework) - path: TestResults - - name: Upload coverage - uses: actions/upload-artifact@v3 - with: - name: Code coverage (.NET Framework) - path: "**/*.cobertura.xml" - - test-examples: - name: Test (Examples) - runs-on: windows-latest - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Save PR number to file - if: github.event_name == 'pull_request' - run: echo ${{ github.event.number }} > PR_NUMBER.txt - - name: Archive PR number - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v3 - with: - name: PR_NUMBER - path: PR_NUMBER.txt - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Build solution - run: dotnet build /p:NetCoreOnly=True --configuration "Release" - - name: Build example solution - run: dotnet build Examples /p:UseFileReferenceToTestablyLibraries=True - - name: Run example tests - uses: Wandalen/wretry.action@v3.7.3 - with: - command: dotnet test Examples --no-build - attempt_limit: 2 - - stryker-ubuntu: - name: Stryker mutation testing (Ubuntu) - runs-on: ubuntu-latest - timeout-minutes: 300 - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Install .NET Stryker - shell: bash - run: | - dotnet tool install dotnet-stryker --tool-path ../tools - - name: Prepare Reports directory - shell: bash - run: | - mkdir Tests/StrykerOutput/Reports -p - - name: Analyze Testably.Abstractions.Testing - env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - shell: bash - run: | - cd Tests - ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.Testing.json -v "${GITHUB_HEAD_REF}" -r "Dashboard" -r "html" -r "cleartext" --since:main - mv ./StrykerOutput/**/reports/*.html ./StrykerOutput/Reports/Testably.Abstractions.Testing-report.html - - name: Upload Stryker reports - uses: actions/upload-artifact@v3 - with: - name: Stryker - path: Tests/StrykerOutput/Reports/* - - stryker-windows: - name: Stryker mutation testing (Windows) - runs-on: windows-latest - timeout-minutes: 300 - steps: - - name: Checkout sources - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 - with: - fetch-depth: 0 - - name: Setup .NET - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 6.0.x - 7.0.x - 8.0.x - - name: Install .NET Stryker - shell: bash - run: | - dotnet tool install dotnet-stryker --tool-path ../tools - - name: Prepare Reports directory - shell: bash - run: | - mkdir Tests/StrykerOutput/Reports -p - - name: Analyze Testably.Abstractions + unit-tests: + name: "Unit tests" + strategy: + matrix: + os: [ ubuntu-latest, windows-latest, macos-latest ] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Run unit tests (windows) + if: matrix.os == 'windows-latest' + run: ./build.ps1 CodeCoverage + - name: Run unit tests (ubuntu|macos) + if: matrix.os != 'windows-latest' + run: ./build.sh CodeCoverage + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: ${{ runner.os }}-artifacts + path: | + ./Artifacts/* + ./TestResults/*.trx + + api-tests: + name: "API tests" + runs-on: ubuntu-latest env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - shell: bash - run: | - cd Tests - ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.json -v "${GITHUB_HEAD_REF}" -r "Dashboard" -r "html" -r "cleartext" --since:main - mv ./StrykerOutput/**/reports/*.html ./StrykerOutput/Reports/Testably.Abstractions-report.html - - name: Analyze Testably.Abstractions.AccessControl + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: API checks + run: ./build.sh ApiChecks + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: API-tests + path: | + ./Artifacts/* + ./TestResults/*.trx + + mutation-tests-linux: + name: "Mutation tests (Linux)" + if: ${{ github.actor != 'dependabot[bot]' }} + runs-on: ubuntu-latest env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - shell: bash - run: | - cd Tests - ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.AccessControl.json -v "${GITHUB_HEAD_REF}" -r "Dashboard" -r "html" -r "cleartext" --since:main - mv ./StrykerOutput/**/reports/*.html ./StrykerOutput/Reports/Testably.Abstractions.AccessControl-report.html - - name: Analyze Testably.Abstractions.Compression + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Run mutation tests + run: ./build.sh MutationTestsLinux + + mutation-tests-windows: + name: "Mutation tests (Windows)" + if: ${{ github.actor != 'dependabot[bot]' }} + runs-on: windows-latest env: - STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} - shell: bash - run: | - cd Tests - ../../tools/dotnet-stryker -f ../.github/stryker/Stryker.Config.Compression.json -v "${GITHUB_HEAD_REF}" -r "Dashboard" -r "html" -r "cleartext" --since:main - mv ./StrykerOutput/**/reports/*.html ./StrykerOutput/Reports/Testably.Abstractions.Compression-report.html - - name: Upload Stryker reports - uses: actions/upload-artifact@v3 - with: - name: Stryker - path: Tests/StrykerOutput/Reports/* - - stryker: - name: Stryker mutation testing result - runs-on: ubuntu-latest - needs: [stryker-windows, stryker-ubuntu] - steps: - - name: Add comment to pull request + STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Run mutation tests + run: ./build.ps1 MutationTestsWindows + + static-code-analysis: + name: "Static code analysis" + if: ${{ github.actor != 'dependabot[bot]' }} + runs-on: ubuntu-latest env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - shell: bash - run: | - prNumber="${{ github.event.number }}" - commentsUrl="https://api.github.com/repos/Testably/Testably.Abstractions/issues/$prNumber/comments" - mutationBadge="[![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2FTestably%2FTestably.Abstractions%2F${GITHUB_HEAD_REF})](https://dashboard.stryker-mutator.io/reports/github.com/Testably/Testably.Abstractions/${GITHUB_HEAD_REF})" - dashboardLink="[Stryker.NET](https://stryker-mutator.io/docs/stryker-net/introduction/) mutation tested the changes in the pull request: \n$mutationBadge" - echo "Search for comment in PR#$prNumber containing $mutationBadge..." - result=$(curl -X GET $commentsUrl \ - -H "Content-Type: application/json" \ - -H "Authorization: token $GITHUB_TOKEN") - if [[ $result != *"$mutationBadge"* ]] - then - body="{\"body\":\"$dashboardLink\"}" - curl -X POST $commentsUrl \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: token $GITHUB_TOKEN" \ - -d "$body" - fi + REPORTGENERATOR_LICENSE: ${{ secrets.REPORTGENERATOR_LICENSE }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + DOTNET_NOLOGO: true + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup .NET SDKs + uses: actions/setup-dotnet@v4 + with: + dotnet-version: | + 6.0.x + 7.0.x + 8.0.x + - name: Run sonarcloud analysis + run: ./build.sh CodeAnalysis + + publish-test-results: + name: "Publish Tests Results" + needs: [ api-tests, unit-tests ] + runs-on: ubuntu-latest + permissions: + checks: write + pull-requests: write + if: always() + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + comment_mode: always + files: "artifacts/**/**/*.trx" diff --git a/.github/workflows/test-report.yml b/.github/workflows/test-report.yml deleted file mode 100644 index 89475b973..000000000 --- a/.github/workflows/test-report.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: 'Test Report' -on: - workflow_run: - workflows: ['CI'] - types: - - completed -jobs: - report: - runs-on: ubuntu-latest - steps: - - uses: dorny/test-reporter@v1 - if: success() || failure() - with: - artifact: Test results (MacOS) - name: MacOS Tests - path: '*.trx' - reporter: dotnet-trx - fail-on-error: 'false' - - uses: dorny/test-reporter@v1 - if: success() || failure() - with: - artifact: Test results (Ubuntu) - name: Ubuntu Tests - path: '*.trx' - reporter: dotnet-trx - fail-on-error: 'false' - - uses: dorny/test-reporter@v1 - if: success() || failure() - with: - artifact: Test results (Windows) - name: Windows Tests - path: '*.trx' - reporter: dotnet-trx - fail-on-error: 'false' - - uses: dorny/test-reporter@v1 - if: success() || failure() - with: - artifact: Test results (.NET Framework) - name: .NET Framework Tests - path: '*.trx' - reporter: dotnet-trx - fail-on-error: 'false' diff --git a/.gitignore b/.gitignore index 1aa97344a..f39d5e3ec 100644 --- a/.gitignore +++ b/.gitignore @@ -6,8 +6,9 @@ # https://learn.microsoft.com/en-us/visualstudio/test/remote-testing /testenvironments.json -# Directory for build output -/Build +# Artifact and test results directories +/Artifacts +/TestResults # Generated files /Tests/*/Generated diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 000000000..daf94b1bd --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,144 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "ApiChecks", + "CalculateNugetVersion", + "Clean", + "CodeAnalysis", + "CodeAnalysisBegin", + "CodeAnalysisEnd", + "CodeCoverage", + "Compile", + "DotNetFrameworkUnitTests", + "DotNetUnitTests", + "MutationTests", + "MutationTestsLinux", + "MutationTestsWindows", + "Pack", + "Restore", + "UnitTests", + "UpdateReadme" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { + "properties": { + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "description": "Host for execution. Default is 'automatic'", + "$ref": "#/definitions/Host" + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "$ref": "#/definitions/ExecutableTarget" + } + }, + "Verbosity": { + "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "GithubToken": { + "type": "string", + "description": "Github Token" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "SonarToken": { + "type": "string", + "description": "The key to push to sonarcloud", + "default": "Secrets must be entered via 'nuke :secrets [profile]'" + } + } + }, + { + "$ref": "#/definitions/NukeBuild" + } + ] +} diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 000000000..db8e1a9cd --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "build.schema.json", + "Solution": "Testably.Abstractions.sln" +} diff --git a/Directory.Packages.props b/Directory.Packages.props index 9b0d8b74d..fe87867e2 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -14,6 +14,12 @@ + + + + + + diff --git a/Examples/Directory.Build.props b/Examples/Directory.Build.props index 2a30f35d5..29135db48 100644 --- a/Examples/Directory.Build.props +++ b/Examples/Directory.Build.props @@ -10,7 +10,6 @@ enable false 701;1702;CA1845;CS7022 - $(SolutionDir)..\Build\Examples\$(MSBuildProjectName) false diff --git a/Pipeline/.editorconfig b/Pipeline/.editorconfig new file mode 100644 index 000000000..31e43dcd8 --- /dev/null +++ b/Pipeline/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/Pipeline/Build.ApiChecks.cs b/Pipeline/Build.ApiChecks.cs new file mode 100644 index 000000000..ce8514736 --- /dev/null +++ b/Pipeline/Build.ApiChecks.cs @@ -0,0 +1,27 @@ +using Nuke.Common; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using static Nuke.Common.Tools.DotNet.DotNetTasks; +// ReSharper disable AllUnderscoreLocalParameterName + +namespace Build; + +partial class Build +{ + Target ApiChecks => _ => _ + .DependsOn(Compile) + .Executes(() => + { + Project project = Solution.Tests.Api.Testably_Abstractions_Api_Tests; + + DotNetTest(s => s + .SetConfiguration(Configuration == Configuration.Debug ? "Debug" : "Release") + .SetProcessEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") + .EnableNoBuild() + .SetResultsDirectory(TestResultsDirectory) + .CombineWith(cc => cc + .SetProjectFile(project) + .AddLoggers($"trx;LogFileName={project.Name}.trx")), completeOnFailure: true); + }); +} diff --git a/Pipeline/Build.CodeAnalysis.cs b/Pipeline/Build.CodeAnalysis.cs new file mode 100644 index 000000000..701d9408a --- /dev/null +++ b/Pipeline/Build.CodeAnalysis.cs @@ -0,0 +1,42 @@ +using Nuke.Common; +using Nuke.Common.Tools.SonarScanner; + +// ReSharper disable AllUnderscoreLocalParameterName + +namespace Build; + +partial class Build +{ + [Parameter("The key to push to sonarcloud")] [Secret] readonly string SonarToken; + + Target CodeAnalysisBegin => _ => _ + .Unlisted() + .Before(Compile) + .Before(CodeCoverage) + .Executes(() => + { + SonarScannerTasks.SonarScannerBegin(s => s + .SetOrganization("testably") + .SetProjectKey("Testably_Testably.Abstractions") + .AddVSTestReports(TestResultsDirectory / "*.trx") + .AddOpenCoverPaths(TestResultsDirectory / "reports" / "OpenCover.xml") + .SetPullRequestOrBranchName(GitHubActions, GitVersion) + .SetVersion(GitVersion.SemVer) + .SetToken(SonarToken)); + }); + + Target CodeAnalysisEnd => _ => _ + .Unlisted() + .DependsOn(Compile) + .DependsOn(CodeCoverage) + .OnlyWhenDynamic(() => IsServerBuild) + .Executes(() => + { + SonarScannerTasks.SonarScannerEnd(s => s + .SetToken(SonarToken)); + }); + + Target CodeAnalysis => _ => _ + .DependsOn(CodeAnalysisBegin) + .DependsOn(CodeAnalysisEnd); +} diff --git a/Pipeline/Build.CodeCoverage.cs b/Pipeline/Build.CodeCoverage.cs new file mode 100644 index 000000000..d43d562c7 --- /dev/null +++ b/Pipeline/Build.CodeCoverage.cs @@ -0,0 +1,25 @@ +using Nuke.Common; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.ReportGenerator; +using static Nuke.Common.Tools.ReportGenerator.ReportGeneratorTasks; + +// ReSharper disable AllUnderscoreLocalParameterName + +namespace Build; + +partial class Build +{ + Target CodeCoverage => _ => _ + .DependsOn(UnitTests) + .Executes(() => + { + ReportGenerator(s => s + .SetProcessToolPath(NuGetToolPathResolver.GetPackageExecutable("ReportGenerator", "ReportGenerator.dll", + framework: "net8.0")) + .SetTargetDirectory(TestResultsDirectory / "reports") + .AddReports(TestResultsDirectory / "**/coverage.cobertura.xml") + .AddReportTypes(ReportTypes.OpenCover) + .AddFileFilters("-*.g.cs") + .SetAssemblyFilters("+Testably*")); + }); +} diff --git a/Pipeline/Build.Compile.cs b/Pipeline/Build.Compile.cs new file mode 100644 index 000000000..739172a4a --- /dev/null +++ b/Pipeline/Build.Compile.cs @@ -0,0 +1,80 @@ +using Nuke.Common; +using Nuke.Common.IO; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Utilities.Collections; +using Nuke.Components; +using Serilog; +using System; +using System.Linq; +using Nuke.Common.Utilities; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +// ReSharper disable AllUnderscoreLocalParameterName + +namespace Build; + +partial class Build +{ + string SemVer; + + Target CalculateNugetVersion => _ => _ + .Unlisted() + .Executes(() => + { + SemVer = GitVersion.SemVer; + + if (GitHubActions?.IsPullRequest == true) + { + string buildNumber = GitHubActions.RunNumber.ToString(); + Console.WriteLine( + $"Branch spec is a pull request. Adding build number {buildNumber}"); + + SemVer = string.Join('.', GitVersion.SemVer.Split('.').Take(3).Union([buildNumber])); + } + + Console.WriteLine($"SemVer = {SemVer}"); + }); + + Target Clean => _ => _ + .Unlisted() + .Before(Restore) + .Executes(() => + { + ArtifactsDirectory.CreateOrCleanDirectory(); + Log.Information("Cleaned {path}...", ArtifactsDirectory); + + TestResultsDirectory.CreateOrCleanDirectory(); + Log.Information("Cleaned {path}...", TestResultsDirectory); + }); + + Target Restore => _ => _ + .Unlisted() + .DependsOn(Clean) + .Executes(() => + { + DotNetRestore(s => s + .SetProjectFile(Solution) + .EnableNoCache() + .SetConfigFile(RootDirectory / "nuget.config")); + }); + + Target Compile => _ => _ + .DependsOn(Restore) + .DependsOn(CalculateNugetVersion) + .Executes(() => + { + ReportSummary(s => s + .WhenNotNull(SemVer, (summary, semVer) => summary + .AddPair("Version", semVer))); + + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .EnableNoLogo() + .EnableNoRestore() + .SetVersion(SemVer) + .SetAssemblyVersion(GitVersion.AssemblySemVer) + .SetFileVersion(GitVersion.AssemblySemFileVer) + .SetInformationalVersion(GitVersion.InformationalVersion)); + }); +} diff --git a/Pipeline/Build.MutationTests.cs b/Pipeline/Build.MutationTests.cs new file mode 100644 index 000000000..2469789a3 --- /dev/null +++ b/Pipeline/Build.MutationTests.cs @@ -0,0 +1,208 @@ +using Nuke.Common; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using Serilog; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +// ReSharper disable AllUnderscoreLocalParameterName + +namespace Build; + +partial class Build +{ + Target MutationTests => _ => _ + .DependsOn(MutationTestsWindows) + .DependsOn(MutationTestsLinux); + + Target MutationTestsWindows => _ => _ + .DependsOn(Compile) + .Executes(() => + { + AbsolutePath toolPath = TestResultsDirectory / "dotnet-stryker"; + AbsolutePath configFile = toolPath / "Stryker.Config.json"; + toolPath.CreateOrCleanDirectory(); + + DotNetToolInstall(_ => _ + .SetPackageName("dotnet-stryker") + .SetToolInstallationPath(toolPath)); + + Dictionary projects = new() + { + { Solution.Testably_Abstractions_AccessControl, [Solution.Tests.Testably_Abstractions_AccessControl_Tests] }, + { Solution.Testably_Abstractions_Compression, [Solution.Tests.Testably_Abstractions_Compression_Tests] }, + { Solution.Testably_Abstractions, [ Solution.Tests.Testably_Abstractions_Testing_Tests, Solution.Tests.Testably_Abstractions_Tests ] } + }; + + foreach (KeyValuePair project in projects) + { + string branchName = GitVersion.BranchName; + if (GitHubActions?.Ref.StartsWith("refs/tags/", StringComparison.OrdinalIgnoreCase) == true) + { + string version = GitHubActions.Ref.Substring("refs/tags/".Length); + branchName = "release/" + version; + Log.Information("Use release branch analysis for '{BranchName}'", branchName); + } + + string configText = $$""" + { + "stryker-config": { + "project-info": { + "name": "github.com/Testably/Testably.Abstractions", + "module": "{{project.Key.Name}}", + "version": "{{branchName}}" + }, + "test-projects": [ + {{string.Join(",\n\t\t\t", project.Value.Select(PathForJson))}} + ], + "project": {{PathForJson(project.Key)}}, + "target-framework": "net8.0", + "since": { + "target": "main", + "enabled": {{(GitVersion.BranchName != "main").ToString().ToLowerInvariant()}}, + "ignore-changes-in": [ + "**/.github/**/*.*" + ] + }, + "reporters": [ + "html", + "progress", + "cleartext" + ], + "mutation-level": "Advanced" + } + } + """; + File.WriteAllText(configFile, configText); + Log.Debug($"Created '{configFile}':{Environment.NewLine}{configText}"); + + string arguments = IsServerBuild + ? $"-f \"{configFile}\" -r \"Dashboard\" -r \"cleartext\"" + : $"-f \"{configFile}\" -r \"cleartext\""; + + string executable = EnvironmentInfo.IsWin ? "dotnet-stryker.exe" : "dotnet-stryker"; + IProcess process = ProcessTasks.StartProcess( + Path.Combine(toolPath, executable), + arguments, + Solution.Directory) + .AssertWaitForExit(); + if (process.ExitCode != 0) + { + Assert.Fail( + $"Stryker did not execute successfully for {project.Key.Name}: (exit code {process.ExitCode})."); + } + } + }); + + Target MutationTestsLinux => _ => _ + .DependsOn(Compile) + .Executes(() => + { + AbsolutePath toolPath = TestResultsDirectory / "dotnet-stryker"; + AbsolutePath configFile = toolPath / "Stryker.Config.json"; + toolPath.CreateOrCleanDirectory(); + + DotNetToolInstall(_ => _ + .SetPackageName("dotnet-stryker") + .SetToolInstallationPath(toolPath)); + + Dictionary projects = new() + { + { Solution.Testably_Abstractions_Testing, [ Solution.Tests.Testably_Abstractions_Testing_Tests, Solution.Tests.Testably_Abstractions_Tests ] }, + }; + + foreach (KeyValuePair project in projects) + { + string branchName = GitVersion.BranchName; + if (GitHubActions?.Ref.StartsWith("refs/tags/", StringComparison.OrdinalIgnoreCase) == true) + { + string version = GitHubActions.Ref.Substring("refs/tags/".Length); + branchName = "release/" + version; + Log.Information("Use release branch analysis for '{BranchName}'", branchName); + } + + string configText = $$""" + { + "stryker-config": { + "project-info": { + "name": "github.com/Testably/Testably.Abstractions", + "module": "{{project.Key.Name}}", + "version": "{{branchName}}" + }, + "test-projects": [ + {{string.Join(",\n\t\t\t", project.Value.Select(PathForJson))}} + ], + "project": {{PathForJson(project.Key)}}, + "target-framework": "net8.0", + "since": { + "target": "main", + "enabled": {{(GitVersion.BranchName != "main").ToString().ToLowerInvariant()}}, + "ignore-changes-in": [ + "**/.github/**/*.*" + ] + }, + "reporters": [ + "html", + "progress", + "cleartext" + ], + "mutation-level": "Advanced", + "mutate": [ + // The enumeration options helper is a wrapper around Microsoft code + "!**/Testably.Abstractions.Testing/Helpers/EnumerationOptionsHelper.cs", + // The exception type is checked, but not the message, as this could be language dependent + "!**/Testably.Abstractions.Testing/Helpers/ExceptionFactory.cs", + // Indicates operating system specific code + "!**/Testably.Abstractions.Testing/Helpers/Execute.cs", + // The directory cleaner should cleanup the real file system + "!**/Testably.Abstractions.Testing/FileSystemInitializer/DirectoryCleaner.cs" + ], + "ignore-methods": [ + // The exception type is checked, but not the message, as this could be language dependent + "ExceptionFactory.*", + // Some checks are redundant but there for performance improvements + "InMemoryLocation.Equals", + // Indicates operating system specific code + "Testably.Abstractions.Testing.Helpers.Execute.*On*", + // Drives are not used in Linux + "ValidateDriveLetter", + // The encryption helper is only valid for testing purposes + "CreateDummyEncryptionAlgorithm", + // Triggered by invalid chars which don't exist in Linux + "ThrowCommonExceptionsIfPathIsInvalid", + // Calls to Thread.Sleep cannot be detected by a test + "System.Threading.Thread.Sleep", + // Ensures that an expectation from developers is met + "Debug.Assert" + ] + } + } + """; + File.WriteAllText(configFile, configText); + Log.Debug($"Created '{configFile}':{Environment.NewLine}{configText}"); + + string arguments = IsServerBuild + ? $"-f \"{configFile}\" -r \"Dashboard\" -r \"cleartext\"" + : $"-f \"{configFile}\" -r \"cleartext\""; + + string executable = EnvironmentInfo.IsWin ? "dotnet-stryker.exe" : "dotnet-stryker"; + IProcess process = ProcessTasks.StartProcess( + Path.Combine(toolPath, executable), + arguments, + Solution.Directory) + .AssertWaitForExit(); + if (process.ExitCode != 0) + { + Assert.Fail( + $"Stryker did not execute successfully for {project.Key.Name}: (exit code {process.ExitCode})."); + } + } + }); + + static string PathForJson(Project project) => $"\"{project.Path.ToString().Replace(@"\", @"\\")}\""; +} diff --git a/Pipeline/Build.Pack.cs b/Pipeline/Build.Pack.cs new file mode 100644 index 000000000..58655405e --- /dev/null +++ b/Pipeline/Build.Pack.cs @@ -0,0 +1,99 @@ +using Nuke.Common; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Utilities.Collections; +using Nuke.Components; +using System.IO; +using System.Linq; +using System.Text; +using Nuke.Common.Utilities; +using static Serilog.Log; + +// ReSharper disable AllUnderscoreLocalParameterName + +namespace Build; + +partial class Build +{ + Target UpdateReadme => _ => _ + .DependsOn(Clean) + .Before(Compile) + .Executes(() => + { + string version = string.Join('.', GitVersion.SemVer.Split('.').Take(3)); + if (version.IndexOf('-') != -1) + { + version = version.Substring(0, version.IndexOf('-')); + } + + StringBuilder sb = new(); + string[] lines = File.ReadAllLines(Solution.Directory / "README.md"); + sb.AppendLine(lines.First()); + sb.AppendLine( + $"[![Changelog](https://img.shields.io/badge/Changelog-v{version}-blue)](https://github.com/Testably/Testably.Abstractions/releases/tag/v{version})"); + foreach (string line in lines.Skip(1)) + { + if (line.StartsWith("[![Build](https://github.com/Testably/Testably.Abstractions/actions/workflows/build.yml") || + line.StartsWith("[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure")) + { + continue; + } + + if (line.StartsWith("[![Coverage](https://sonarcloud.io/api/project_badges/measure")) + { + sb.AppendLine(line + .Replace(")", $"&branch=release/v{version})")); + continue; + } + + if (line.StartsWith("[![Mutation testing badge](https://img.shields.io/endpoint")) + { + sb.AppendLine(line + .Replace("%2Fmain)", $"%2Frelease%2Fv{version})") + .Replace("/main)", $"/release/v{version})")); + continue; + } + + sb.AppendLine(line); + } + + File.WriteAllText(ArtifactsDirectory / "README.md", sb.ToString()); + }); + + Target Pack => _ => _ + .DependsOn(UpdateReadme) + .DependsOn(Compile) + .Executes(() => + { + ReportSummary(s => s + .WhenNotNull(SemVer, (c, semVer) => c + .AddPair("Packed version", semVer))); + + AbsolutePath packagesDirectory = ArtifactsDirectory / "Packages"; + packagesDirectory.CreateOrCleanDirectory(); + + foreach (Project project in new[] + { + Solution.Testably_Abstractions, + Solution.Testably_Abstractions_AccessControl, + Solution.Testably_Abstractions_Compression, + Solution.Testably_Abstractions_Interface, + Solution.Testably_Abstractions_Testing, + }) + { + foreach (string package in + Directory.EnumerateFiles(project.Directory / "bin", "*.nupkg", SearchOption.AllDirectories)) + { + File.Move(package, packagesDirectory / Path.GetFileName(package)); + Debug("Found nuget package: {PackagePath}", package); + } + + foreach (string symbolPackage in + Directory.EnumerateFiles(project.Directory / "bin", "*.snupkg", SearchOption.AllDirectories)) + { + File.Move(symbolPackage, packagesDirectory / Path.GetFileName(symbolPackage)); + Debug("Found symbol package: {PackagePath}", symbolPackage); + } + } + }); +} diff --git a/Pipeline/Build.UnitTest.cs b/Pipeline/Build.UnitTest.cs new file mode 100644 index 000000000..ee612a3f3 --- /dev/null +++ b/Pipeline/Build.UnitTest.cs @@ -0,0 +1,76 @@ +using Nuke.Common; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tooling; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.Xunit; +using System.Linq; +using static Nuke.Common.Tools.Xunit.XunitTasks; +using static Nuke.Common.Tools.DotNet.DotNetTasks; + +// ReSharper disable AllUnderscoreLocalParameterName + +namespace Build; + +partial class Build +{ + Target UnitTests => _ => _ + .DependsOn(DotNetFrameworkUnitTests) + .DependsOn(DotNetUnitTests); + + Project[] UnitTestProjects => + [ + Solution.Tests.Testably_Abstractions_Parity_Tests, + Solution.Tests.Testably_Abstractions_Tests, + Solution.Tests.Testably_Abstractions_Testing_Tests, + Solution.Tests.Testably_Abstractions_Compression_Tests, + Solution.Tests.Testably_Abstractions_AccessControl_Tests + ]; + + Target DotNetFrameworkUnitTests => _ => _ + .Unlisted() + .DependsOn(Compile) + .OnlyWhenDynamic(() => EnvironmentInfo.IsWin) + .Executes(() => + { + string[] testAssemblies = UnitTestProjects + .SelectMany(project => + project.Directory.GlobFiles( + $"bin/{(Configuration == Configuration.Debug ? "Debug" : "Release")}/net48/*.Tests.dll")) + .Select(p => p.ToString()) + .ToArray(); + + Assert.NotEmpty(testAssemblies.ToList()); + + Xunit2(s => s + .SetFramework("net48") + .AddTargetAssemblies(testAssemblies) + ); + }); + + Target DotNetUnitTests => _ => _ + .Unlisted() + .DependsOn(Compile) + .Executes(() => + { + string net48 = "net48"; + DotNetTest(s => s + .SetConfiguration(Configuration) + .SetProcessEnvironmentVariable("DOTNET_CLI_UI_LANGUAGE", "en-US") + .EnableNoBuild() + .SetDataCollector("XPlat Code Coverage") + .SetResultsDirectory(TestResultsDirectory) + .CombineWith( + UnitTestProjects, + (settings, project) => settings + .SetProjectFile(project) + .CombineWith( + project.GetTargetFrameworks()?.Except([net48]), + (frameworkSettings, framework) => frameworkSettings + .SetFramework(framework) + .AddLoggers($"trx;LogFileName={project.Name}_{framework}.trx") + ) + ), completeOnFailure: true + ); + }); +} diff --git a/Pipeline/Build.cs b/Pipeline/Build.cs new file mode 100644 index 000000000..ffd925f51 --- /dev/null +++ b/Pipeline/Build.cs @@ -0,0 +1,34 @@ +using Nuke.Common; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.GitVersion; + +namespace Build; + +[GitHubActions( + "Build", + GitHubActionsImage.UbuntuLatest, + AutoGenerate = false, + ImportSecrets = [nameof(GithubToken)] +)] +partial class Build : NukeBuild +{ + [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [Parameter("Github Token")] readonly string GithubToken; + + [Required] [GitVersion(Framework = "net8.0", NoCache = true, NoFetch = true)] readonly GitVersion GitVersion; + + [Solution(GenerateProjects = true)] readonly Solution Solution; + + AbsolutePath ArtifactsDirectory => RootDirectory / "Artifacts"; + AbsolutePath TestResultsDirectory => RootDirectory / "TestResults"; + GitHubActions GitHubActions => GitHubActions.Instance; + + public static int Main() => Execute([ + x => x.ApiChecks, + x => x.UnitTests, + ]); +} diff --git a/Pipeline/Build.csproj b/Pipeline/Build.csproj new file mode 100644 index 000000000..ab852ed22 --- /dev/null +++ b/Pipeline/Build.csproj @@ -0,0 +1,38 @@ + + + + Exe + net8.0 + CS0649;CS0169;CA1050;CA1822;CA2211;IDE1006 + .. + .. + 1 + false + + + OS_WINDOWS + + + OS_LINUX + + + OS_MAC + + + + + + + + + + + + + + + + + + + diff --git a/Pipeline/Build.csproj.DotSettings b/Pipeline/Build.csproj.DotSettings new file mode 100644 index 000000000..c815d363e --- /dev/null +++ b/Pipeline/Build.csproj.DotSettings @@ -0,0 +1,31 @@ + + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + DO_NOT_SHOW + Implicit + Implicit + ExpressionBody + 0 + NEXT_LINE + True + False + 120 + IF_OWNER_IS_SINGLE_LINE + WRAP_IF_LONG + False + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + True + True + True + True + True + True + True + True + True + True diff --git a/Pipeline/BuildExtensions.cs b/Pipeline/BuildExtensions.cs new file mode 100644 index 000000000..c43c5d1bd --- /dev/null +++ b/Pipeline/BuildExtensions.cs @@ -0,0 +1,36 @@ +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.Tools.GitVersion; +using Nuke.Common.Tools.SonarScanner; +using Serilog; +using System; + +namespace Build; + +public static class BuildExtensions +{ + public static SonarScannerBeginSettings SetPullRequestOrBranchName( + this SonarScannerBeginSettings settings, + GitHubActions gitHubActions, + GitVersion gitVersion) + { + if (gitHubActions?.IsPullRequest == true) + { + Log.Information("Use pull request analysis"); + return settings + .SetPullRequestKey(gitHubActions.PullRequestNumber.ToString()) + .SetPullRequestBranch(gitHubActions.Ref) + .SetPullRequestBase(gitHubActions.BaseRef); + } + + if (gitHubActions?.Ref.StartsWith("refs/tags/", StringComparison.OrdinalIgnoreCase) == true) + { + string version = gitHubActions.Ref.Substring("refs/tags/".Length); + string branchName = "release/" + version; + Log.Information("Use release branch analysis for '{BranchName}'", branchName); + return settings.SetBranchName(branchName); + } + + Log.Information("Use branch analysis for '{BranchName}'", gitVersion.BranchName); + return settings.SetBranchName(gitVersion.BranchName); + } +} diff --git a/Pipeline/Configuration.cs b/Pipeline/Configuration.cs new file mode 100644 index 000000000..9541f050f --- /dev/null +++ b/Pipeline/Configuration.cs @@ -0,0 +1,16 @@ +using Nuke.Common.Tooling; +using System.ComponentModel; + +namespace Build; + +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static Configuration Debug = new Configuration { Value = nameof(Debug) }; + public static Configuration Release = new Configuration { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} \ No newline at end of file diff --git a/Pipeline/Directory.Build.props b/Pipeline/Directory.Build.props new file mode 100644 index 000000000..e147d6352 --- /dev/null +++ b/Pipeline/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Pipeline/Directory.Build.targets b/Pipeline/Directory.Build.targets new file mode 100644 index 000000000..253260956 --- /dev/null +++ b/Pipeline/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/Source/Directory.Build.props b/Source/Directory.Build.props index b1f940527..9a5e32899 100644 --- a/Source/Directory.Build.props +++ b/Source/Directory.Build.props @@ -22,7 +22,6 @@ enable 1701;1702 true - ..\..\Build\Binaries diff --git a/Testably.Abstractions.sln b/Testably.Abstractions.sln index 6a4d696e2..cd5518a55 100644 --- a/Testably.Abstractions.sln +++ b/Testably.Abstractions.sln @@ -18,6 +18,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "_", "_", "{94F99274-3518-45 nuget.config = nuget.config README.md = README.md renovate.json = renovate.json + build.cmd = build.cmd + build.ps1 = build.ps1 + build.sh = build.sh EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{61F335A6-9CE0-4040-A34F-E70B1A55077D}" @@ -48,19 +51,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{5E35 .github\mergify.yml = .github\mergify.yml EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "stryker", "stryker", "{4D8D7391-1E7B-4051-AD7E-4086AFD4E024}" - ProjectSection(SolutionItems) = preProject - .github\stryker\Stryker.Config.AccessControl.json = .github\stryker\Stryker.Config.AccessControl.json - .github\stryker\Stryker.Config.Compression.json = .github\stryker\Stryker.Config.Compression.json - .github\stryker\Stryker.Config.json = .github\stryker\Stryker.Config.json - .github\stryker\Stryker.Config.Testing.json = .github\stryker\Stryker.Config.Testing.json - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{8FD8E33B-2072-48CA-8164-DE2AA5DCB8FD}" ProjectSection(SolutionItems) = preProject .github\workflows\build.yml = .github\workflows\build.yml .github\workflows\ci.yml = .github\workflows\ci.yml - .github\workflows\test-report.yml = .github\workflows\test-report.yml EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testably.Abstractions.Parity.Tests", "Tests\Testably.Abstractions.Parity.Tests\Testably.Abstractions.Parity.Tests.csproj", "{E9A42D82-0609-4D6B-B270-A30176B4FCF2}" @@ -100,6 +94,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Testably.Abstractions.TestS EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Settings", "Settings", "{D32170F4-1455-4839-8EF4-9530F3AA642A}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Pipeline", "Pipeline", "{6C54481B-E85D-4207-9FE7-F4FBE7325DB0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Build", "Pipeline\Build.csproj", "{5C03DA5C-4F3B-44F5-ACED-28B38B5EE68F}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -162,6 +160,8 @@ Global {241E6413-819C-4B06-A739-4D10C2A81365}.Debug|Any CPU.Build.0 = Debug|Any CPU {241E6413-819C-4B06-A739-4D10C2A81365}.Release|Any CPU.ActiveCfg = Release|Any CPU {241E6413-819C-4B06-A739-4D10C2A81365}.Release|Any CPU.Build.0 = Release|Any CPU + {5C03DA5C-4F3B-44F5-ACED-28B38B5EE68F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5C03DA5C-4F3B-44F5-ACED-28B38B5EE68F}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -172,7 +172,6 @@ Global {7545AC28-1399-4AF5-9C63-0507F78F017A} = {94F99274-3518-45D0-83A4-6F370D6FE74B} {2FDB2AAE-E5CE-483E-A1A6-EC0B0A4AD67B} = {94F99274-3518-45D0-83A4-6F370D6FE74B} {5E35E265-7110-47A0-9E3E-B5180BBB5AA6} = {94F99274-3518-45D0-83A4-6F370D6FE74B} - {4D8D7391-1E7B-4051-AD7E-4086AFD4E024} = {5E35E265-7110-47A0-9E3E-B5180BBB5AA6} {8FD8E33B-2072-48CA-8164-DE2AA5DCB8FD} = {5E35E265-7110-47A0-9E3E-B5180BBB5AA6} {E9A42D82-0609-4D6B-B270-A30176B4FCF2} = {61F335A6-9CE0-4040-A34F-E70B1A55077D} {1B078385-7AAA-4B8D-A88E-2CBCD3011F0D} = {61F335A6-9CE0-4040-A34F-E70B1A55077D} @@ -186,6 +185,7 @@ Global {0AEBB1A6-07D6-46DC-BC78-9771D380EBC0} = {2FDB2AAE-E5CE-483E-A1A6-EC0B0A4AD67B} {241E6413-819C-4B06-A739-4D10C2A81365} = {D32170F4-1455-4839-8EF4-9530F3AA642A} {D32170F4-1455-4839-8EF4-9530F3AA642A} = {61F335A6-9CE0-4040-A34F-E70B1A55077D} + {5C03DA5C-4F3B-44F5-ACED-28B38B5EE68F} = {6C54481B-E85D-4207-9FE7-F4FBE7325DB0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EC4D8481-B9FD-41B5-832A-369210993DF4} diff --git a/Tests/Api/Directory.Build.props b/Tests/Api/Directory.Build.props index c8e3e039b..5d1cdaeed 100644 --- a/Tests/Api/Directory.Build.props +++ b/Tests/Api/Directory.Build.props @@ -14,7 +14,6 @@ enable false 701;1702;CA1845 - ..\..\..\Build\Tests\$(MSBuildProjectName) diff --git a/Tests/Api/Testably.Abstractions.Api.Tests/Helper.cs b/Tests/Api/Testably.Abstractions.Api.Tests/Helper.cs index ddcb92cba..80d4ae5f0 100644 --- a/Tests/Api/Testably.Abstractions.Api.Tests/Helper.cs +++ b/Tests/Api/Testably.Abstractions.Api.Tests/Helper.cs @@ -13,7 +13,12 @@ public static class Helper { public static string CreatePublicApi(string framework, string assemblyName) { - string assemblyFile = CombinedPaths("Build", "Binaries", framework, $"{assemblyName}.dll"); +#if DEBUG + string configuration = "Debug"; +#else + string configuration = "Release"; +#endif + string assemblyFile = CombinedPaths("Source", assemblyName, "bin", configuration, framework, $"{assemblyName}.dll"); Assembly assembly = Assembly.LoadFile(assemblyFile); string publicApi = assembly.GeneratePublicApi(options: null); return publicApi.Replace("\r\n", "\n"); diff --git a/Tests/Directory.Build.props b/Tests/Directory.Build.props index c0e595d08..7f7938f37 100644 --- a/Tests/Directory.Build.props +++ b/Tests/Directory.Build.props @@ -16,7 +16,6 @@ enable false 701;1702;CA1845;MA0004 - ..\..\Build\Tests\$(MSBuildProjectName) diff --git a/Tests/Helpers/Testably.Abstractions.TestHelpers/Testably.Abstractions.TestHelpers.csproj b/Tests/Helpers/Testably.Abstractions.TestHelpers/Testably.Abstractions.TestHelpers.csproj index a62987979..26cd2db4d 100644 --- a/Tests/Helpers/Testably.Abstractions.TestHelpers/Testably.Abstractions.TestHelpers.csproj +++ b/Tests/Helpers/Testably.Abstractions.TestHelpers/Testably.Abstractions.TestHelpers.csproj @@ -9,10 +9,6 @@ false - - ..\..\..\Build\Tests\$(MSBuildProjectName) - - diff --git a/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/Testably.Abstractions.Tests.SourceGenerator.csproj b/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/Testably.Abstractions.Tests.SourceGenerator.csproj index 496dbb151..5f539b4d7 100644 --- a/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/Testably.Abstractions.Tests.SourceGenerator.csproj +++ b/Tests/Helpers/Testably.Abstractions.Tests.SourceGenerator/Testably.Abstractions.Tests.SourceGenerator.csproj @@ -3,7 +3,6 @@ netstandard2.0 latest - ..\..\..\Build\Tests\$(MSBuildProjectName) true diff --git a/Tests/Settings/Directory.Build.props b/Tests/Settings/Directory.Build.props index c8e3e039b..5d1cdaeed 100644 --- a/Tests/Settings/Directory.Build.props +++ b/Tests/Settings/Directory.Build.props @@ -14,7 +14,6 @@ enable false 701;1702;CA1845 - ..\..\..\Build\Tests\$(MSBuildProjectName) diff --git a/build.cmd b/build.cmd new file mode 100755 index 000000000..b08cc590f --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 new file mode 100644 index 000000000..8b218a560 --- /dev/null +++ b/build.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +Param( + [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] + [string[]]$BuildArguments +) + +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" + +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent + +########################################################################### +# CONFIGURATION +########################################################################### + +$BuildProjectFile = "$PSScriptRoot\Pipeline\Build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" + +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "STS" + +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_NOLOGO = 1 + +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path +} +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) + + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } + } + + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" +} + +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null +} + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh new file mode 100755 index 000000000..46f2bac0a --- /dev/null +++ b/build.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash + +bash --version 2>&1 | head -n 1 + +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) + +########################################################################### +# CONFIGURATION +########################################################################### + +BUILD_PROJECT_FILE="$SCRIPT_DIR/Pipeline/Build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" + +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="STS" + +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_NOLOGO=1 + +########################################################################### +# EXECUTION +########################################################################### + +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi + fi + + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" +fi + +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" + +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi + +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"