tasks/dotnet.tasks.ps1
# Control flags $CleanBuild = $false $EnableCoverage = $true $SkipTest = $false $SkipTestReport = $false $SkipSolutionPackages = $false $SkipNuspecPackages = $false $SkipProjectPublishPackages = $false # Options $SolutionToBuild = $false $ProjectsToPublish = @() $NuSpecFilesToPackage = @() $SkipSolutionPackages = $false $SkipProjectPublishPackages = $false $SkipNuspecPackages = $false $FoldersToClean = @("bin", "obj", "TestResults", "_codeCoverage", "_packages") $ReportGeneratorToolVersion = "4.8.3" $TestReportTypes = "Cobertura" # Ensure we resolve a relative path for $PackagesDir now, rather than letting it be resolved by downstream tooling # For example, 'dotnet pack' will resolve it relative to a given project which is not what we want. $PackagesDir = [IO.Path]::IsPathRooted($PackagesDir) ? $PackagesDir : (Join-Path $here $PackagesDir) # Logging Options $_defaultDotNetTestLogger = "console;verbosity=$LogLevel" # we store this so we can tell whether 'DotNetTestLogger' has been customised $DotNetTestLogger = $_defaultDotNetTestLogger # Provides a backwards-compatible mechanism for supporting multiple test loggers $DotNetTestLoggers = @() $DotNetFileLoggerVerbosity = "detailed" $DotNetCompileLogFile = "dotnet-build.log" $DotNetCompileFileLoggerProps = "/flp:verbosity=$DotNetFileLoggerVerbosity;logfile=$DotNetCompileLogFile" $DotNetTestLogFile = "dotnet-test.log" $DotNetTestFileLoggerProps = "/flp:verbosity=$DotNetFileLoggerVerbosity;logfile=$DotNetTestLogFile" $DotNetPackageLogFile = "dotnet-package.log" $DotNetPackageFileLoggerProps = "/flp:verbosity=$DotNetFileLoggerVerbosity;logfile=$DotNetPackageLogFile" $DotNetPackageNuSpecLogFile = "dotnet-pacakge-nuspec.log" $DotNetPackageNuSpecFileLoggerProps = "/flp:verbosity=$DotNetFileLoggerVerbosity;logfile=$DotNetPackageNuSpecLogFile;append" $DotNetPublishLogFile = "dotnet-publish.log" $DotNetPublishFileLoggerProps = "/flp:verbosity=$DotNetFileLoggerVerbosity;logfile=$DotNetPublishLogFile;append" # NuGet Publishing Options $NugetPublishSource = "$here/_local-nuget-feed" $NugetPublishSymbolSource = $NugetPublishSource $NugetPublishSkipDuplicates = $true # By default the build will publish all NuGet packages it finds with the current version number $NugetPackageNamesToPublishGlob = "*" # Uses a non-interpolated string to ensure lazy evaluation of the GitVersion variable $NugetPackagesToPublishGlobSuffix = '.$(($script:GitVersion).SemVer).nupkg' # Synopsis: Clean .NET solution task CleanSolution -If {$CleanBuild -and $SolutionToBuild} { exec { dotnet clean $SolutionToBuild ` --configuration $Configuration ` --verbosity $LogLevel } # Delete output folders Write-Build White "Deleting output folders..." $FoldersToClean | ForEach-Object { Get-ChildItem -Path (Split-Path -Parent $SolutionToBuild) ` -Filter $_ ` -Recurse ` | Where-Object { $_.PSIsContainer } } | Remove-Item -Recurse -Force } # Synopsis: Build .NET solution task BuildSolution -If {$SolutionToBuild} Version,RestorePackages,{ exec { try { dotnet build $SolutionToBuild ` --no-restore ` --configuration $Configuration ` /p:Version="$(($script:GitVersion).SemVer)" ` /p:EndjinRepositoryUrl="$BuildRepositoryUri" ` --verbosity $LogLevel ` $($DotNetCompileFileLoggerProps ? $DotNetCompileFileLoggerProps : "/fl") } finally { if ((Test-Path $DotNetCompileLogFile) -and $IsAzureDevOps) { Write-Host "##vso[artifact.upload artifactname=logs]$(Get-Item $DotNetCompileLogFile)" } } } } # Synopsis: Restore .NET Solution Packages task RestorePackages -If {$SolutionToBuild} CleanSolution,{ exec { dotnet restore $SolutionToBuild ` --verbosity $LogLevel } } # Synopsis: Build .NET solution packages task BuildSolutionPackages -If {!$SkipSolutionPackages -and $SolutionToBuild} Version,{ exec { try { # Change use of '--output' - ref: https://github.com/dotnet/sdk/issues/30624#issuecomment-1432118204 dotnet pack $SolutionToBuild ` --configuration $Configuration ` --no-build ` --no-restore ` /p:PackageOutputPath="$PackagesDir" ` /p:EndjinRepositoryUrl="$BuildRepositoryUri" ` /p:PackageVersion="$(($script:GitVersion).SemVer)" ` --verbosity $LogLevel ` $($DotNetPackageFileLoggerProps ? $DotNetPackageFileLoggerProps : "/fl") } finally { if ((Test-Path $DotNetPackageLogFile) -and $IsAzureDevOps) { Write-Host "##vso[artifact.upload artifactname=logs]$(Get-Item $DotNetPackageLogFile)" } } } } # Synopsis: Run .NET solution tests task RunTests -If {!$SkipTest -and $SolutionToBuild} { # Only setup the default CI/CD platform test loggers if they haven't already been customised if ($DotNetTestLoggers.Count -eq 0 -and $DotNetTestLogger -eq $_defaultDotNetTestLogger) { if ($script:IsAzureDevOps) { Write-Build Green "Configuring Azure Pipelines test logger" $DotNetTestLogger = "AzurePipelines" } elseif ($script:IsGitHubActions) { Write-Build Green "Configuring GitHub Actions test logger" $DotNetTestLogger = "GitHubActions" } } # Setup the arguments we need to pass to 'dotnet test' $dotnetTestArgs = @( "--configuration", $Configuration "--no-build" "--no-restore" '/p:CollectCoverage="{0}"' -f $EnableCoverage "/p:CoverletOutputFormat=cobertura" '/p:ExcludeByFile="{0}"' -f $ExcludeFilesFromCodeCoverage.Replace(",","%2C") "--verbosity", $LogLevel "--test-adapter-path", "$PSScriptRoot/../bin" ($DotNetTestFileLoggerProps ? $DotNetTestFileLoggerProps : "/fl") ) # If multiple test loggers have been specified then use that newer config property if ($DotNetTestLoggers.Count -gt 0) { $DotNetTestLoggers | ForEach-Object { $dotnetTestArgs += @("--logger", $_) } } # Otherwise fallback to the original behaviour so we are backwards-compatible else { $dotnetTestArgs += @("--logger", $DotNetTestLogger) } try { exec { dotnet test $SolutionToBuild @dotnetTestArgs } } finally { if ((Test-Path $DotNetTestLogFile) -and $IsAzureDevOps) { Write-Host "##vso[artifact.upload artifactname=logs]$(Get-Item $DotNetTestLogFile)" } } } # Synopsis: Generate test report file task GenerateTestReport -If {!$SkipTest -and !$SkipTestReport -and $SolutionToBuild} { Install-DotNetTool -Name "dotnet-reportgenerator-globaltool" -Version $ReportGeneratorToolVersion $testReportGlob = "$SourcesDir/**/**/coverage.cobertura.xml" if (!(Get-ChildItem -Path $SourceDir -Filter "coverage.cobertura.xml" -Recurse)) { Write-Warning "No code coverage reports found for the file pattern '$testReportGlob' - skipping test report" } else { exec { reportgenerator "-reports:$testReportGlob" ` "-targetdir:$CoverageDir" ` "-reporttypes:$TestReportTypes" } } } # Synopsis: Build publish packages for selected projects task BuildProjectPublishPackages -If {!$SkipProjectPublishPackages -and $ProjectsToPublish} Version,{ # Remove the existing log, since we append to it for each project being published Get-Item $DotNetPublishLogFile -ErrorAction Ignore | Remove-Item -Force # Check each entry to see whether it is using the older or newer configuration style $projectPublishingTasks = $ProjectsToPublish | % { if ($_ -is [Hashtable]) { # New style config: just use whatever has been specified $_ } else { # Old style config: generate a configuration that will mimic the previous behaviour @{ Project = $_; RuntimeIdentifiers = @('NOT_SPECIFIED'); SelfContained = $false; Trimmed = $false; ReadyToRun = $false } } } try { foreach ($task in $projectPublishingTasks) { foreach ($runtime in $task.RuntimeIdentifiers) { $optionalCmdArgs = @() if ($task.ContainsKey("Trimmed") -and $task.Trimmed -eq $true) { $optionalCmdArgs += "-p:PublishTrimmed=true" } if ($task.ContainsKey("ReadyToRun") -and $task.ReadyToRun -eq $true) { $optionalCmdArgs += "-p:PublishReadyToRun=true" } if ($task.ContainsKey("SingleFile") -and $task.SingleFile -eq $true) { $optionalCmdArgs += "-p:PublishSingleFile=true" } if ($runtime -eq "NOT_SPECIFIED") { # If no runtime is specified then we can skip the build $optionalCmdArgs += "--no-build" } else { # Specify the required runtime $optionalCmdArgs += "--runtime",$runtime # When specifying a runtime, you need to explicitly flag it as self-contained or not $optionalCmdArgs += (($task.ContainsKey("SelfContained") -and $task.SelfContained -eq $true) ? "--self-contained" : "--no-self-contained") } Write-Build Green "Publishing Project: $($task.Project) [$($runtime)] [SelfContained=$($task.SelfContained)] [SingleFile=$($task.SingleFile)] [Trimmed=$($task.Trimmed)] [ReadyToRun=$($task.ReadyToRun)]" $packageOutputDir = Join-Path $PackagesDir $(Split-Path -LeafBase $task.Project) ($runtime -eq "NOT_SPECIFIED" ? "" : $runtime) exec { # Change use of '--output' - ref: https://github.com/dotnet/sdk/issues/30624#issuecomment-1432118204 dotnet publish $task.Project ` --nologo ` --configuration $Configuration ` --no-restore ` /p:PublishDir="$packageOutputDir" ` /p:EndjinRepositoryUrl="$BuildRepositoryUri" ` /p:PackageVersion="$(($script:GitVersion).SemVer)" ` --verbosity $LogLevel ` @optionalCmdArgs ` $($DotNetPublishFileLoggerProps ? $DotNetPublishFileLoggerProps : "/fl") } } } } finally { if ((Test-Path $DotNetPublishLogFile) -and $IsAzureDevOps) { Write-Host "##vso[artifact.upload artifactname=logs]$(Get-Item $DotNetPublishLogFile)" } } } # Synopsis: Publish any built NuGet packages task PublishSolutionPackages -If {!$SkipSolutionPackages -and $SolutionToBuild -and $NugetPackageNamesToPublishGlob} Version,{ # Force the wildcard expression to be evaluated now that GitVersion has been run $evaluatedNugetPackagesToPublishGlob = Invoke-Expression "`"$($NugetPackageNamesToPublishGlob)$($NugetPackagesToPublishGlobSuffix)`"" Write-Verbose "EvaluatedNugetPackagesToPublishGlob: $evaluatedNugetPackagesToPublishGlob" $nugetPackagesToPublish = Get-ChildItem -Path "$here/_packages" -Filter $evaluatedNugetPackagesToPublishGlob Write-Verbose "NugetPackagesToPublish: $nugetPackagesToPublish" # Derive the NuGet API key to use - this also makes it easier to mask later on # NOTE: Where NuGet auth has been setup beforehand (e.g. via a SOURCE), an API key still needs to be specified but it can be any value $nugetApiKey = $env:NUGET_API_KEY ? $env:NUGET_API_KEY : "no-key" # Setup the 'dotnet nuget push' command-line parameters that will be the same for each package $nugetPushArgs = @( "-s" $NugetPublishSource "-ss" $NugetPublishSymbolSource "--api-key" $nugetApiKey ) if ($NugetPublishSkipDuplicates) { $nugetPushArgs += @( "--skip-duplicate" ) } # Remove the existing log, since we append to it for each project being packaged via a NuSpec file Get-Item $DotNetPackageNuSpecLogFile -ErrorAction Ignore | Remove-Item -Force try { foreach ($nugetPackage in $nugetPackagesToPublish) { Write-Build Green "Publishing package: $nugetPackage" # Ensure any NuGet API key is masked in the debug logging Write-Verbose ("dotnet nuget push $nugetPackage $nugetPushArgs".Replace($nugetApiKey, "*****")) exec { & dotnet nuget push $nugetPackage $nugetPushArgs } } } finally { if ((Test-Path $DotNetPackageNuSpecLogFile) -and $IsAzureDevOps) { Write-Host "##vso[artifact.upload artifactname=logs]$(Get-Item $DotNetPackageNuSpecLogFile)" } } } # Synopsis: Build any .nuspec based NuGet Packages task BuildNuSpecPackages -If {!$SkipNuspecPackages -and $SolutionToBuild -and $NuspecFilesToPackage} Version,{ foreach ($nuspec in $NuSpecFilesToPackage) { # Assumes a convention that the .nuspec file is alongside the .csproj file with a matching name $nuspecFilePath = (Resolve-Path (Join-Path $here $nuspec)).Path $projectFilePath = $nuspecFilePath.Replace(".nuspec", ".csproj") Write-Build Green "Packaging NuSpec: $nuspecFilePath [Project=$projectFilePath]" $packArgs = @( "--nologo" $projectFilePath "--configuration" $Configuration "--no-build" "--no-restore" # ref: https://github.com/dotnet/sdk/issues/30624#issuecomment-1432118204 "-p:PackageOutputPath=$PackagesDir" # this property needs to be overridden as its default value should be 'false', to ensure that the project # is not built without using the .nuspec file "-p:IsPackable=true" "-p:NuspecFile=$nuspecFilePath" "-p:NuspecProperties=version=`"$(($script:GitVersion).SemVer)`"" "--verbosity" $LogLevel $($DotNetPackageNuSpecFileLoggerProps ? $DotNetPackageNuSpecFileLoggerProps : "/fl") $DotNetPackageNuSpecFileLoggerProps ) Write-Verbose "dotnet pack $packArgs" exec { & dotnet pack $packArgs } } } |