blob: 6a821398ccb5de7847780a15738036735fd5c1c7 [file] [log] [blame]
# 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 }}