| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| name: CI |
| |
| on: |
| workflow_dispatch: |
| push: |
| branches: |
| - main |
| - 'release/v*.*' # matches release/v1.2 |
| - 'release/v*.*.*' # matches release/v1.2.3 |
| - 'release-workflow*' # special branch names for testing release workflow (this file) from a PR |
| tags: |
| - 'v*' |
| pull_request: |
| release: |
| types: [published] |
| |
| jobs: |
| build: |
| if: | |
| github.event_name == 'workflow_dispatch' || |
| github.event_name == 'pull_request' || |
| (github.event_name == 'push' && |
| (startsWith(github.ref, 'refs/heads/main') || |
| startsWith(github.ref, 'refs/heads/releases/v') || |
| github.ref_type == 'tag')) |
| runs-on: ubuntu-latest |
| env: |
| DIST_DIR: ${{ github.workspace }}/dist |
| NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages |
| steps: |
| - name: Checkout Source |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 |
| with: |
| fetch-depth: 0 |
| fetch-tags: true |
| |
| - name: Setup .NET 8.0 |
| uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 |
| with: |
| dotnet-version: 8.0.x |
| |
| - name: Cache NuGet Packages |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 |
| with: |
| # '**/*.*proj' includes .csproj, .vbproj, .fsproj, msbuildproj, etc. |
| # '**/*.props' includes Directory.Packages.props and Directory.Build.props |
| # '**/*.targets' includes Directory.Build.targets |
| # '**/*.sln' and '*.sln' ensure root solution files are included (minimatch glitch for file extension .sln) |
| # 'global.json' included for SDK version changes |
| key: nuget-v1-${{ runner.os }}-${{ hashFiles('**/*.*proj', '**/*.props', '**/*.targets', '**/*.sln', '*.sln', 'global.json') }} |
| path: ${{ env.NUGET_PACKAGES }} |
| |
| - name: Restore |
| run: dotnet restore |
| |
| - name: Build |
| shell: pwsh |
| run: | |
| $nugetOut = Join-Path $env:DIST_DIR 'nuget' |
| dotnet build -c Release --no-restore -p:PackageOutputPath=$nugetOut |
| |
| - name: Publish Test Assemblies |
| shell: pwsh |
| run: | |
| $testsOut = Join-Path $env:DIST_DIR 'testBinaries' |
| New-Item -ItemType Directory -Force -Path $testsOut | Out-Null |
| |
| # Find all csproj files where a segment is exactly 'Tests' |
| Get-ChildItem -Recurse -Filter *.csproj | ForEach-Object { |
| Write-Host "Found project: $($_.FullName)" |
| $segments = $_.BaseName -split '\.' |
| if ($segments -contains 'Tests') { |
| $projName = $_.BaseName |
| $outDir = Join-Path $testsOut $projName |
| New-Item -ItemType Directory -Force -Path $outDir | Out-Null |
| Write-Host "Publishing $($_.FullName) -> $outDir" |
| dotnet publish $_.FullName -c Release --no-build -o $outDir --verbosity normal |
| if ($LASTEXITCODE -ne 0) { throw "Publish failed for $($_.FullName)" } |
| } |
| } |
| |
| - name: Upload NuGet packages |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 |
| with: |
| name: nuget-packages |
| path: ${{ env.DIST_DIR }}/nuget |
| |
| - name: Upload Test Assemblies |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 |
| with: |
| name: test-assemblies |
| path: ${{ env.DIST_DIR }}/testBinaries |
| |
| test: |
| needs: build |
| strategy: |
| matrix: |
| # macos-13 is specifically for running x64, macos-latest for arm64 |
| os: [windows-latest, ubuntu-latest, macos-13, macos-latest] |
| arch: [x64, arm64] |
| tfm: [net8.0] |
| exclude: |
| - arch: arm64 |
| os: macos-13 |
| - arch: arm64 |
| os: windows-latest |
| - arch: arm64 |
| os: ubuntu-latest |
| - arch: x64 |
| os: macos-latest |
| fail-fast: false |
| |
| runs-on: ${{ matrix.os }} |
| |
| steps: |
| - name: Checkout Source |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 |
| |
| # Set up working directories/paths consistently |
| - name: Set Paths (Windows) |
| if: runner.os == 'Windows' |
| shell: pwsh |
| run: | |
| $dir = "C:\w" |
| $testResultsRootDir = "$dir\work\test-results\${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.tfm }}" |
| $coverageReportDir = "$testResultsRootDir\coverage-report" |
| New-Item -ItemType Directory -Force -Path $dir |
| New-Item -ItemType Directory -Force -Path "$dir\dotnet" |
| New-Item -ItemType Directory -Force -Path "$dir\work" |
| New-Item -ItemType Directory -Force -Path "$testResultsRootDir" |
| New-Item -ItemType Directory -Force -Path "$coverageReportDir" |
| Add-Content $env:GITHUB_ENV "`nDOTNET_INSTALL_DIR=$dir\dotnet" |
| Add-Content $env:GITHUB_ENV "`nWORKPATH=$dir\work" |
| Add-Content $env:GITHUB_ENV "`nTEST_RESULTS_ROOT_DIR=$testResultsRootDir" |
| Add-Content $env:GITHUB_ENV "`nCOVERAGE_REPORT_DIR=$coverageReportDir" |
| Add-Content $env:GITHUB_PATH "`n$($env:USERPROFILE)\.dotnet\tools" |
| |
| - name: Set Paths (Linux/macOS) |
| if: runner.os == 'Linux' || runner.os == 'macOS' |
| shell: pwsh |
| run: | |
| $dir = "$env:RUNNER_TEMP/w" |
| $testResultsRootDir = "$dir/work/test-results/${{ matrix.os }}-${{ matrix.arch }}-${{ matrix.tfm }}" |
| $coverageReportDir = "$testResultsRootDir/coverage-report" |
| New-Item -ItemType Directory -Force -Path $dir |
| New-Item -ItemType Directory -Force -Path "$dir/dotnet" |
| New-Item -ItemType Directory -Force -Path "$dir/work" |
| New-Item -ItemType Directory -Force -Path "$testResultsRootDir" |
| New-Item -ItemType Directory -Force -Path "$coverageReportDir" |
| Add-Content $env:GITHUB_ENV "`nDOTNET_INSTALL_DIR=$dir/dotnet" |
| Add-Content $env:GITHUB_ENV "`nWORKPATH=$dir/work" |
| Add-Content $env:GITHUB_ENV "`nTEST_RESULTS_ROOT_DIR=$testResultsRootDir" |
| Add-Content $env:GITHUB_ENV "`nCOVERAGE_REPORT_DIR=$coverageReportDir" |
| Add-Content $env:GITHUB_PATH "`n$($env:HOME)/.dotnet/tools" |
| |
| # Install the .NET SDK |
| - name: Setup .NET 8.0 |
| uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 |
| with: |
| dotnet-version: 8.0.x |
| |
| - name: Download Test Binaries |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 |
| with: |
| name: test-assemblies |
| path: ${{ env.WORKPATH }} |
| |
| - name: Install Coverage |
| shell: pwsh |
| run: dotnet tool install --global dotnet-coverage |
| |
| - name: Install ReportGenerator |
| shell: pwsh |
| run: dotnet tool install --global dotnet-reportgenerator-globaltool |
| |
| - name: Run Tests |
| shell: pwsh |
| run: | |
| # Imports |
| . (Join-Path $env:GITHUB_WORKSPACE 'eng' 'build' 'Markdown-Formatting.ps1') |
| |
| $includesCoverage = $false |
| $results = @() |
| |
| Get-ChildItem -Directory $env:WORKPATH | ForEach-Object { |
| $dllPath = Join-Path $_.FullName ($_.Name + ".dll") |
| $resultsDirectory = Join-Path $env:TEST_RESULTS_ROOT_DIR $_.Name |
| New-Item -ItemType Directory -Force -Path $resultsDirectory | Out-Null |
| |
| if (Test-Path $dllPath) { |
| Write-Host "Running tests in $dllPath..." |
| |
| # Build as array of arguments (excluding the "dotnet" prefix and -- arguments) |
| $testCmd = @( |
| "test", $dllPath, |
| "--framework", "${{ matrix.tfm }}", |
| "--logger:console;verbosity=normal", |
| "--logger:trx;LogFileName=TestResults.trx", |
| "--results-directory", $resultsDirectory, |
| "--blame-hang-timeout", "10m", |
| "--blame-hang-dump-type", "mini", |
| "--blame-crash" |
| ) |
| |
| if ("${{ matrix.arch }}" -eq 'arm64' -and ($env:RUNNER_OS -eq 'macOS' -or $env:RUNNER_OS -eq 'Linux')) { |
| # Run tests without coverage |
| dotnet @testCmd -- RunConfiguration.TargetPlatform=${{ matrix.arch }} |
| } else { |
| # Ensure a coverage folder per test project output |
| $coverageDir = Join-Path $resultsDirectory "coverage" |
| New-Item -ItemType Directory -Force -Path $coverageDir | Out-Null |
| $coverageFile = Join-Path $coverageDir "coverage.xml" |
| |
| # Collect coverage in Cobertura format; TRX is produced by the inner 'dotnet test' |
| dotnet-coverage collect "dotnet $($testCmd -join ' ') -- RunConfiguration.TargetPlatform=${{ matrix.arch }}" ` |
| --output-format cobertura ` |
| --output "$coverageFile" ` |
| --settings (Join-Path "$env:GITHUB_WORKSPACE" 'eng' 'build' 'coverage.runsettings') |
| |
| if (Test-Path $coverageFile) { $includesCoverage = $true } |
| } |
| |
| # TRX inspection / status computation |
| $trxFile = Join-Path $resultsDirectory "TestResults.trx" |
| if (Test-Path $trxFile) { |
| $parsed = & "$env:GITHUB_WORKSPACE/eng/build/Parse-Test-Results.ps1" -Path $trxFile |
| $parsed | Add-Member -NotePropertyName "SuiteName" -NotePropertyValue $_.Name |
| $results += $parsed |
| } |
| } |
| } |
| |
| # Write summary |
| Format-Test-Results $results | Add-Content $env:GITHUB_STEP_SUMMARY |
| |
| if ($includesCoverage) { |
| # Generate a per-job coverage summary + HTML reports from all coverage.xml files created above |
| $reportsGlob = Join-Path $env:TEST_RESULTS_ROOT_DIR "**/coverage/coverage.xml" |
| reportgenerator ` |
| "-reports:$reportsGlob" ` |
| "-targetdir:$env:COVERAGE_REPORT_DIR" ` |
| "-reporttypes:MarkdownSummaryGithub;HtmlInline_AzurePipelines;HtmlChart" ` |
| "-verbosity:Warning" |
| |
| # Append collapsible coverage section to the job summary |
| $md = Join-Path $env:COVERAGE_REPORT_DIR "SummaryGithub.md" |
| if (Test-Path $md) { |
| Add-Content $env:GITHUB_STEP_SUMMARY "`n<details><summary>Coverage</summary>`n" |
| Get-Content $md | Add-Content $env:GITHUB_STEP_SUMMARY |
| Add-Content $env:GITHUB_STEP_SUMMARY "`n</details>`n" |
| } else { |
| Write-Host "Coverage summary not generated (no coverage.xml found)." |
| } |
| } |
| |
| # Report test failure |
| if ($results | Where-Object { $_.FailedCount -gt 0 -or $_.Crashed }) { |
| exit 1 |
| } |
| |
| - name: Upload Test Results |
| if: always() |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 |
| with: |
| name: test-results-${{ matrix.os }}-${{ matrix.arch }} |
| path: ${{ env.TEST_RESULTS_ROOT_DIR }} |
| |
| coverage: |
| name: aggregate-coverage |
| needs: test |
| runs-on: ubuntu-latest |
| steps: |
| - name: Download all test result artifacts |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 |
| with: |
| pattern: test-results-* |
| path: all-results |
| merge-multiple: true |
| |
| # Required for ReportGenerator |
| - name: Setup .NET SDK |
| uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d # v5 |
| with: |
| dotnet-version: 8.0.x |
| |
| - name: Install ReportGenerator |
| shell: pwsh |
| run: | |
| dotnet tool install --global dotnet-reportgenerator-globaltool |
| Add-Content $env:GITHUB_PATH "`n$HOME/.dotnet/tools" |
| |
| - name: Build Aggregate Report |
| shell: pwsh |
| run: | |
| New-Item -ItemType Directory -Force -Path "coverage/aggregate" | Out-Null |
| reportgenerator ` |
| "-reports:all-results/**/coverage/coverage.xml" ` |
| "-targetdir:coverage/aggregate" ` |
| "-reporttypes:MarkdownSummaryGithub;HtmlInline_AzurePipelines;HtmlChart" ` |
| "-verbosity:Warning" |
| |
| $md = "coverage/aggregate/SummaryGithub.md" |
| if (Test-Path $md) { |
| Add-Content $env:GITHUB_STEP_SUMMARY "## Aggregate Coverage`n" |
| Get-Content $md | Add-Content $env:GITHUB_STEP_SUMMARY |
| } else { |
| Add-Content $env:GITHUB_STEP_SUMMARY "## Aggregate Coverage`n_No coverage files found._" |
| } |
| |
| - name: Upload Aggregate Coverage HTML |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 |
| with: |
| name: coverage-aggregate-html |
| path: coverage/aggregate |
| |
| release: |
| name: release |
| if: github.event_name == 'push' && github.ref_type == 'tag' |
| runs-on: ubuntu-latest |
| needs: |
| - build |
| - test |
| steps: |
| - name: Checkout Source |
| uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5 |
| with: |
| fetch-depth: 0 |
| fetch-tags: true |
| |
| - name: Get Tag Version |
| id: tagversion |
| shell: pwsh |
| run: | |
| $tag = '${{ github.ref_name }}' -replace '^v', '' |
| Add-Content $env:GITHUB_OUTPUT "version=$tag" |
| |
| # prerelease = true if contains a hyphen (semver prerelease part) |
| $prerelease = if ($tag -match '-') { 'true' } else { 'false' } |
| Add-Content $env:GITHUB_OUTPUT "`nprerelease=$prerelease" |
| |
| - name: Get Version from NBGV |
| id: nbgv |
| shell: pwsh |
| run: | |
| $nugetVersion = & nbgv get-version -v NuGetPackageVersion |
| Add-Content $env:GITHUB_OUTPUT "version=$nugetVersion" |
| |
| - name: Download NuGet Packages |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5 |
| with: |
| name: nuget-packages |
| path: dist/nuget |
| - name: Verify Versions Match |
| shell: pwsh |
| run: | |
| $tagVersion = '${{ steps.tagversion.outputs.version }}' |
| $repoVersion = '${{ steps.nbgv.outputs.version }}' |
| |
| if ($tagVersion -ne $repoVersion) { |
| throw "Tag version $tagVersion does not match repository version $repoVersion. Make sure there are no typos in the Git tag name." |
| } |
| |
| # Also check that all .nupkg files contain the version |
| Get-ChildItem dist/nuget/*.nupkg | ForEach-Object { |
| if (-not $_.Name.Contains($tagVersion)) { |
| throw "Artifact version mismatch: $($_.Name) vs tag version $tagVersion" |
| } |
| } |
| - name: Create Draft GitHub Release |
| uses: softprops/action-gh-release@6da8fa9354ddfdc4aeace5fc48d7f679b5214090 # v2 |
| with: |
| draft: true |
| prerelease: ${{ steps.tagversion.outputs.prerelease }} |
| files: dist/nuget/*.nupkg,dist/nuget/*.snupkg |
| generate_release_notes: true |
| token: ${{ secrets.GITHUB_TOKEN }} |
| |
| publish: |
| name: publish |
| if: github.event_name == 'release' && github.event.action == 'published' |
| runs-on: ubuntu-latest |
| steps: |
| - name: Download Release Assets |
| uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8 |
| with: |
| script: | |
| const fs = require("fs"); |
| const path = require("path"); |
| |
| const release = context.payload.release; |
| if (!release) { |
| core.setFailed("No release payload found."); |
| return; |
| } |
| |
| const assets = release.assets || []; |
| const dir = "dist/nuget"; |
| fs.mkdirSync(dir, { recursive: true }); |
| |
| for (const asset of assets) { |
| if (!asset.name.endsWith(".nupkg") && !asset.name.endsWith(".snupkg")) { |
| continue; |
| } |
| core.info(`Downloading ${asset.name}...`); |
| const response = await github.request("GET /repos/{owner}/{repo}/releases/assets/{asset_id}", { |
| owner: context.repo.owner, |
| repo: context.repo.repo, |
| asset_id: asset.id, |
| headers: { Accept: "application/octet-stream" }, |
| }); |
| const filePath = path.join(dir, asset.name); |
| fs.writeFileSync(filePath, Buffer.from(response.data)); |
| core.info(`Saved to ${filePath}`); |
| } |
| - name: Push To NuGet |
| shell: pwsh |
| run: | |
| $files = Get-ChildItem "dist/nuget/*.nupkg", "dist/nuget/*.snupkg" -ErrorAction Ignore |
| foreach ($file in $files) { |
| dotnet nuget push $file.FullName --source $env:NUGET_SOURCE_URL --api-key $env:NUGET_API_KEY --skip-duplicate |
| } |
| env: |
| NUGET_SOURCE_URL: 'https://api.nuget.org/v3/index.json' |
| NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} |