diff --git a/.gitignore b/.gitignore index 65f947769..3bb499198 100644 --- a/.gitignore +++ b/.gitignore @@ -140,6 +140,7 @@ _TeamCity* # Visual Studio code coverage results *.coverage *.coveragexml +/coveragereport/ # NCrunch _NCrunch_* diff --git a/Directory.Build.props b/Directory.Build.props index 50865cda8..762d2dd09 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -23,7 +23,7 @@ embedded true - $(MSBuildThisFileDirectory)\strongname.snk + $(MSBuildThisFileDirectory)strongname.snk Andrew Arnott aarnott @@ -42,9 +42,9 @@ - + - + @@ -81,7 +81,7 @@ It's also not necessary to generate these assets. --> - <_WpfTempProjectNuGetFilePathNoExt>$(RepoRootPath)obj\$(_TargetAssemblyProjectName)\$(_TargetAssemblyProjectName)$(MSBuildProjectExtension).nuget.g + <_WpfTempProjectNuGetFilePathNoExt>$(BaseIntermediateOutputPath)..\$(_TargetAssemblyProjectName)\$(_TargetAssemblyProjectName)$(MSBuildProjectExtension).nuget.g false false diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dd346ab8e..2d2e4e35f 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,6 +13,12 @@ trigger: - .github/ - azure-pipelines/release.yml +parameters: +- name: RunTests + displayName: Run tests + type: boolean + default: true + resources: containers: - container: xenial @@ -37,9 +43,12 @@ stages: - stage: Build jobs: - template: azure-pipelines/build.yml + parameters: + RunTests: ${{ parameters.RunTests }} - stage: Test displayName: Functional testing + condition: and(succeeded(), ${{ parameters.RunTests }}) jobs: - job: linux strategy: diff --git a/azure-pipelines/Get-CodeCovTool.ps1 b/azure-pipelines/Get-CodeCovTool.ps1 new file mode 100644 index 000000000..ca580b4db --- /dev/null +++ b/azure-pipelines/Get-CodeCovTool.ps1 @@ -0,0 +1,86 @@ +<# +.SYNOPSIS + Downloads the CodeCov.io uploader tool and returns the path to it. +.PARAMETER AllowSkipVerify + Allows skipping signature verification of the downloaded tool if gpg is not installed. +#> +[CmdletBinding()] +Param( + [switch]$AllowSkipVerify +) + +if ($IsMacOS) { + $codeCovUrl = "https://uploader.codecov.io/latest/macos/codecov" + $toolName = 'codecov' +} +elseif ($IsLinux) { + $codeCovUrl = "https://uploader.codecov.io/latest/linux/codecov" + $toolName = 'codecov' +} +else { + $codeCovUrl = "https://uploader.codecov.io/latest/windows/codecov.exe" + $toolName = 'codecov.exe' +} + +$shaSuffix = ".SHA256SUM" +$sigSuffix = $shaSuffix + ".sig" + +Function Get-FileFromWeb([Uri]$Uri, $OutDir) { + $OutFile = Join-Path $OutDir $Uri.Segments[-1] + if (!(Test-Path $OutFile)) { + Write-Verbose "Downloading $Uri..." + if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } + try { + (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) + } finally { + # This try/finally causes the script to abort + } + } + + $OutFile +} + +$toolsPath = & "$PSScriptRoot\Get-TempToolsPath.ps1" +$binaryToolsPath = Join-Path $toolsPath codecov +$testingPath = Join-Path $binaryToolsPath unverified +$finalToolPath = Join-Path $binaryToolsPath $toolName + +if (!(Test-Path $finalToolPath)) { + if (Test-Path $testingPath) { + Remove-Item -Recurse -Force $testingPath # ensure we download all matching files + } + $tool = Get-FileFromWeb $codeCovUrl $testingPath + $sha = Get-FileFromWeb "$codeCovUrl$shaSuffix" $testingPath + $sig = Get-FileFromWeb "$codeCovUrl$sigSuffix" $testingPath + $key = Get-FileFromWeb https://keybase.io/codecovsecurity/pgp_keys.asc $testingPath + + if ((Get-Command gpg -ErrorAction SilentlyContinue)) { + Write-Host "Importing codecov key" -ForegroundColor Yellow + gpg --import $key + Write-Host "Verifying signature on codecov hash" -ForegroundColor Yellow + gpg --verify $sig $sha + } else { + if ($AllowSkipVerify) { + Write-Warning "gpg not found. Unable to verify hash signature." + } else { + throw "gpg not found. Unable to verify hash signature. Install gpg or add -AllowSkipVerify to override." + } + } + + Write-Host "Verifying hash on downloaded tool" -ForegroundColor Yellow + $actualHash = (Get-FileHash -Path $tool -Algorithm SHA256).Hash + $expectedHash = (Get-Content $sha).Split()[0] + if ($actualHash -ne $expectedHash) { + # Validation failed. Delete the tool so we can't execute it. + #Remove-Item $codeCovPath + throw "codecov uploader tool failed signature validation." + } + + Copy-Item $tool $finalToolPath + + if ($IsMacOS -or $IsLinux) { + chmod u+x $finalToolPath + } +} + +return $finalToolPath diff --git a/azure-pipelines/Get-SymbolFiles.ps1 b/azure-pipelines/Get-SymbolFiles.ps1 index fccb1bb13..0ce229fc2 100644 --- a/azure-pipelines/Get-SymbolFiles.ps1 +++ b/azure-pipelines/Get-SymbolFiles.ps1 @@ -18,7 +18,7 @@ Write-Progress -Activity $ActivityName -CurrentOperation "Discovery PDB files" $PDBs = Get-ChildItem -rec "$Path/*.pdb" # Filter PDBs to product OR test related. -$testregex = "unittest|tests" +$testregex = "unittest|tests|\.test\." Write-Progress -Activity $ActivityName -CurrentOperation "De-duplicating symbols" $PDBsByHash = @{} @@ -49,8 +49,13 @@ $PDBs |% { $BinaryImagePath = $dllPath } elseif (Test-Path $exePath) { $BinaryImagePath = $exePath + } else { + Write-Warning "`"$_`" found with no matching binary file." + $BinaryImagePath = $null } - Write-Output $BinaryImagePath - Write-Output $_.FullName + if ($BinaryImagePath) { + Write-Output $BinaryImagePath + Write-Output $_.FullName + } } diff --git a/azure-pipelines/Merge-CodeCoverage.ps1 b/azure-pipelines/Merge-CodeCoverage.ps1 new file mode 100644 index 000000000..91ab67ab3 --- /dev/null +++ b/azure-pipelines/Merge-CodeCoverage.ps1 @@ -0,0 +1,46 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Merges code coverage reports. +.PARAMETER Path + The path(s) to search for Cobertura code coverage reports. +.PARAMETER Format + The format for the merged result. The default is Cobertura +.PARAMETER OutputDir + The directory the merged result will be written to. The default is `coveragereport` in the root of this repo. +#> +[CmdletBinding()] +Param( + [Parameter(Mandatory=$true)] + [string[]]$Path, + [ValidateSet('Badges', 'Clover', 'Cobertura', 'CsvSummary', 'Html', 'Html_Dark', 'Html_Light', 'HtmlChart', 'HtmlInline', 'HtmlInline_AzurePipelines', 'HtmlInline_AzurePipelines_Dark', 'HtmlInline_AzurePipelines_Light', 'HtmlSummary', 'JsonSummary', 'Latex', 'LatexSummary', 'lcov', 'MarkdownSummary', 'MHtml', 'PngChart', 'SonarQube', 'TeamCitySummary', 'TextSummary', 'Xml', 'XmlSummary')] + [string]$Format='Cobertura', + [string]$OutputDir=("$PSScriptRoot/../coveragereport") +) + +$RepoRoot = [string](Resolve-Path $PSScriptRoot/..) + +if (!(Test-Path $RepoRoot/obj/reportgenerator*)) { + dotnet tool install --tool-path $RepoRoot/obj dotnet-reportgenerator-globaltool --version 5.1.9 --configfile $PSScriptRoot/justnugetorg.nuget.config +} + +Write-Verbose "Searching $Path for *.cobertura.xml files" +$reports = Get-ChildItem -Recurse $Path -Filter *.cobertura.xml + +if ($reports) { + $reports |% { $_.FullName } |% { + # In addition to replacing {reporoot}, we also normalize on one kind of slash so that the report aggregates data for a file whether data was collected on Windows or not. + $xml = [xml](Get-Content -Path $_) + $xml.coverage.packages.package.classes.class |? { $_.filename} |% { + $_.filename = $_.filename.Replace('{reporoot}', $RepoRoot).Replace([IO.Path]::AltDirectorySeparatorChar, [IO.Path]::DirectorySeparatorChar) + } + + $xml.Save($_) + } + + $Inputs = [string]::join(';', ($reports |% { Resolve-Path -relative $_.FullName })) + & "$RepoRoot/obj/reportgenerator" -reports:"$Inputs" -targetdir:$OutputDir -reporttypes:$Format +} else { + Write-Error "No reports found to merge." +} diff --git a/azure-pipelines/artifacts/_all.ps1 b/azure-pipelines/artifacts/_all.ps1 index c9182a45a..033cc87c9 100644 --- a/azure-pipelines/artifacts/_all.ps1 +++ b/azure-pipelines/artifacts/_all.ps1 @@ -15,6 +15,7 @@ Executes artifact scripts even if they have already been staged. #> +[CmdletBinding(SupportsShouldProcess = $true)] param ( [string]$ArtifactNameSuffix, [switch]$Force diff --git a/azure-pipelines/artifacts/_pipelines.ps1 b/azure-pipelines/artifacts/_pipelines.ps1 index a62d2675b..2d3338b24 100644 --- a/azure-pipelines/artifacts/_pipelines.ps1 +++ b/azure-pipelines/artifacts/_pipelines.ps1 @@ -1,6 +1,10 @@ -# This script translates all the artifacts described by _all.ps1 -# into commands that instruct Azure Pipelines to actually collect those artifacts. +<# +.SYNOPSIS + This script translates all the artifacts described by _all.ps1 + into commands that instruct Azure Pipelines to actually collect those artifacts. +#> +[CmdletBinding()] param ( [string]$ArtifactNameSuffix, [switch]$StageOnly diff --git a/azure-pipelines/artifacts/_stage_all.ps1 b/azure-pipelines/artifacts/_stage_all.ps1 index b7166b4eb..d81d16d46 100644 --- a/azure-pipelines/artifacts/_stage_all.ps1 +++ b/azure-pipelines/artifacts/_stage_all.ps1 @@ -1,6 +1,10 @@ -# This script links all the artifacts described by _all.ps1 -# into a staging directory, reading for uploading to a cloud build artifact store. -# It returns a sequence of objects with Name and Path properties. +<# +.SYNOPSIS + This script links all the artifacts described by _all.ps1 + into a staging directory, reading for uploading to a cloud build artifact store. + It returns a sequence of objects with Name and Path properties. +#> + [CmdletBinding()] param ( [string]$ArtifactNameSuffix diff --git a/azure-pipelines/artifacts/coverageResults.ps1 b/azure-pipelines/artifacts/coverageResults.ps1 index 8fdb3f720..280ff9ae0 100644 --- a/azure-pipelines/artifacts/coverageResults.ps1 +++ b/azure-pipelines/artifacts/coverageResults.ps1 @@ -1,10 +1,11 @@ $RepoRoot = [System.IO.Path]::GetFullPath("$PSScriptRoot\..\..") +$coverageFiles = @(Get-ChildItem "$RepoRoot/test/*.cobertura.xml" -Recurse | Where {$_.FullName -notlike "*/In/*" -and $_.FullName -notlike "*\In\*" }) + # Prepare code coverage reports for merging on another machine if ($env:SYSTEM_DEFAULTWORKINGDIRECTORY) { Write-Host "Substituting $env:SYSTEM_DEFAULTWORKINGDIRECTORY with `"{reporoot}`"" - $reports = Get-ChildItem "$RepoRoot/bin/coverage.*cobertura.xml" -Recurse - $reports |% { + $coverageFiles |% { $content = Get-Content -Path $_ |% { $_ -Replace [regex]::Escape($env:SYSTEM_DEFAULTWORKINGDIRECTORY), "{reporoot}" } Set-Content -Path $_ -Value $content -Encoding UTF8 } @@ -16,7 +17,7 @@ if (!((Test-Path $RepoRoot\bin) -and (Test-Path $RepoRoot\obj))) { return } @{ $RepoRoot = ( - @(Get-ChildItem "$RepoRoot\bin\coverage.*cobertura.xml" -Recurse) + + $coverageFiles + (Get-ChildItem "$RepoRoot\obj\*.cs" -Recurse) ); } diff --git a/azure-pipelines/artifacts/testResults.ps1 b/azure-pipelines/artifacts/testResults.ps1 index 2f894c97b..301a43763 100644 --- a/azure-pipelines/artifacts/testResults.ps1 +++ b/azure-pipelines/artifacts/testResults.ps1 @@ -1,16 +1,11 @@ +[CmdletBinding()] +Param( +) + $result = @{} -if ($env:AGENT_TEMPDIRECTORY) { - # The DotNetCoreCLI uses an alternate location to publish these files - $guidRegex = '^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$' - $result[$env:AGENT_TEMPDIRECTORY] = (Get-ChildItem $env:AGENT_TEMPDIRECTORY -Directory |? { $_.Name -match $guidRegex } |% { - Get-ChildItem "$($_.FullName)\dotnet*.dmp","$($_.FullName)\*_crashdump.dmp","$($_.FullName)\testhost*.dmp","$($_.FullName)\Sequence_*.xml" -Recurse - }); -} -else { - $testRoot = Resolve-Path "$PSScriptRoot\..\..\test" - $result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File) -} +$testRoot = Resolve-Path "$PSScriptRoot\..\..\test" +$result[$testRoot] = (Get-ChildItem "$testRoot\TestResults" -Recurse -Directory | Get-ChildItem -Recurse -File) $testlogsPath = "$env:BUILD_ARTIFACTSTAGINGDIRECTORY\test_logs" if (Test-Path $testlogsPath) { diff --git a/azure-pipelines/build.yml b/azure-pipelines/build.yml index 019cb2e30..bc31259c6 100644 --- a/azure-pipelines/build.yml +++ b/azure-pipelines/build.yml @@ -3,6 +3,9 @@ parameters: type: object default: vmImage: windows-2022 +- name: RunTests + type: boolean + default: true jobs: - job: Windows @@ -21,24 +24,26 @@ jobs: Invoke-WebRequest -Uri "https://dot.net/v1/dotnet-install.ps1" -OutFile dotnet-install.ps1 & .\dotnet-install.ps1 -Architecture x86 -Channel 3.1 -InstallDir "C:\Program Files (x86)\dotnet\" -NoPath -Verbose -Runtime dotnet & .\dotnet-install.ps1 -Architecture x86 -Version 6.0.301 -InstallDir "C:\Program Files (x86)\dotnet\" -NoPath -Verbose - displayName: Install 32-bit .NET SDK and runtimes + displayName: โš™ Install 32-bit .NET SDK and runtimes - powershell: '& (./azure-pipelines/Get-nbgv.ps1) cloud -c' - displayName: Set build number + displayName: โš™ Set build number - template: dotnet.yml + parameters: + RunTests: ${{ parameters.RunTests }} - job: Linux pool: vmImage: Ubuntu 20.04 - variables: - testModifier: -f net6.0 steps: - checkout: self clean: true submodules: true # keep the warnings quiet about the wiki not being enlisted - template: install-dependencies.yml - template: dotnet.yml + parameters: + RunTests: ${{ parameters.RunTests }} - job: WrapUp dependsOn: @@ -52,5 +57,6 @@ jobs: - template: install-dependencies.yml parameters: initArgs: -NoRestore - - template: publish-codecoverage.yml + - ${{ if parameters.RunTests }}: + - template: publish-codecoverage.yml - template: publish-deployables.yml diff --git a/azure-pipelines/dotnet-test-cloud.ps1 b/azure-pipelines/dotnet-test-cloud.ps1 index 43cdc3e00..24bf812a1 100644 --- a/azure-pipelines/dotnet-test-cloud.ps1 +++ b/azure-pipelines/dotnet-test-cloud.ps1 @@ -48,7 +48,8 @@ if ($x86) { --no-build ` -c $Configuration ` --filter "TestCategory!=FailsInCloudTest" ` - -p:CollectCoverage=true ` + --collect "Code Coverage;Format=cobertura" ` + --settings "$PSScriptRoot/test.runsettings" ` --blame-hang-timeout 60s ` --blame-crash ` -bl:"$ArtifactStagingFolder/build_logs/test.binlog" ` diff --git a/azure-pipelines/dotnet.yml b/azure-pipelines/dotnet.yml index cf5969443..8e3415259 100644 --- a/azure-pipelines/dotnet.yml +++ b/azure-pipelines/dotnet.yml @@ -1,3 +1,6 @@ +parameters: + RunTests: + steps: - script: | @@ -6,7 +9,7 @@ steps: displayName: Configure git commit author for testing - script: dotnet build -t:build,pack --no-restore -c $(BuildConfiguration) /bl:"$(Build.ArtifactStagingDirectory)/build_logs/build.binlog" - displayName: dotnet build + displayName: ๐Ÿ›  dotnet build - script: dotnet pack -c $(BuildConfiguration) --no-build -p:PackLKG=true /bl:"$(Build.ArtifactStagingDirectory)/build_logs/msbuild_lkg.binlog" displayName: Build LKG package @@ -21,10 +24,11 @@ steps: workingDirectory: src/nerdbank-gitversioning.npm - powershell: azure-pipelines/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults - displayName: dotnet test + displayName: ๐Ÿงช dotnet test + condition: and(succeeded(), ${{ parameters.RunTests }}) - powershell: azure-pipelines/dotnet-test-cloud.ps1 -Configuration $(BuildConfiguration) -Agent $(Agent.JobName) -PublishResults -X86 - displayName: dotnet test x86 + displayName: ๐Ÿงช dotnet test x86 condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT')) - powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" -StageOnly @@ -44,7 +48,7 @@ steps: --secret '$(codesign_secret)' --name 'Nerdbank.GitVersioning' --descriptionUrl 'https://github.com/dotnet/Nerdbank.GitVersioning' - displayName: Code sign + displayName: ๐Ÿ” Code sign condition: and(succeeded(), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), eq(variables['Agent.OS'], 'Windows_NT'), ne(variables['Build.Reason'], 'PullRequest')) - pwsh: > @@ -57,21 +61,24 @@ steps: --secret '$(codesign_secret)' --name 'Nerdbank.GitVersioning' --descriptionUrl 'https://github.com/dotnet/Nerdbank.GitVersioning' - displayName: Code sign LKG + displayName: ๐Ÿ” Code sign LKG condition: and(succeeded(), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), eq(variables['Agent.OS'], 'Windows_NT'), ne(variables['Build.Reason'], 'PullRequest')) - powershell: azure-pipelines/variables/_pipelines.ps1 failOnStderr: true - displayName: Update pipeline variables based on build outputs + displayName: โš™ Update pipeline variables based on build outputs condition: succeededOrFailed() -- powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" +- powershell: azure-pipelines/artifacts/_pipelines.ps1 -ArtifactNameSuffix "-$(Agent.JobName)" -Verbose failOnStderr: true - displayName: Publish artifacts + displayName: ๐Ÿ“ข Publish artifacts condition: succeededOrFailed() -- bash: bash <(curl -s https://codecov.io/bash) - displayName: Publish code coverage results to codecov.io - condition: ne(variables['codecov_token'], '') - timeoutInMinutes: 3 - continueOnError: true +- ${{ if and(ne(variables['codecov_token'], ''), parameters.RunTests) }}: + - powershell: | + $ArtifactStagingFolder = & "azure-pipelines/Get-ArtifactsStagingDirectory.ps1" + $CoverageResultsFolder = Join-Path $ArtifactStagingFolder "coverageResults-$(Agent.JobName)" + azure-pipelines/publish-CodeCov.ps1 -CodeCovToken "$(codecov_token)" -PathToCodeCoverage "$CoverageResultsFolder" -Name "$(Agent.JobName) Coverage Results" -Flags "$(Agent.JobName)Host,$(BuildConfiguration)" + displayName: ๐Ÿ“ข Publish code coverage results to codecov.io + timeoutInMinutes: 3 + continueOnError: true diff --git a/azure-pipelines/install-dependencies.yml b/azure-pipelines/install-dependencies.yml index 4f848b099..3993f1043 100644 --- a/azure-pipelines/install-dependencies.yml +++ b/azure-pipelines/install-dependencies.yml @@ -4,7 +4,7 @@ parameters: steps: - task: NuGetAuthenticate@0 - displayName: Authenticate NuGet feeds + displayName: ๐Ÿ” Authenticate NuGet feeds inputs: forceReinstallCredentialProvider: true @@ -17,9 +17,9 @@ steps: if (Get-Command mono -ErrorAction SilentlyContinue) { mono --version } - displayName: Install prerequisites + displayName: โš™ Install prerequisites - powershell: azure-pipelines/variables/_pipelines.ps1 failOnStderr: true - displayName: Set pipeline variables based on source + displayName: โš™ Set pipeline variables based on source name: SetPipelineVariables diff --git a/azure-pipelines/publish-CodeCov.ps1 b/azure-pipelines/publish-CodeCov.ps1 new file mode 100644 index 000000000..9926f0188 --- /dev/null +++ b/azure-pipelines/publish-CodeCov.ps1 @@ -0,0 +1,30 @@ +<# +.SYNOPSIS + Uploads code coverage to codecov.io +.PARAMETER CodeCovToken + Code coverage token to use +.PARAMETER PathToCodeCoverage + Path to root of code coverage files +.PARAMETER Name + Name to upload with codecoverge +.PARAMETER Flags + Flags to upload with codecoverge +#> +[CmdletBinding()] +Param ( + [Parameter(Mandatory=$true)] + [string]$CodeCovToken, + [Parameter(Mandatory=$true)] + [string]$PathToCodeCoverage, + [string]$Name, + [string]$Flags +) + +$RepoRoot = (Resolve-Path "$PSScriptRoot/..").Path + +Get-ChildItem -Recurse -Path $PathToCodeCoverage -Filter "*.cobertura.xml" | % { + $relativeFilePath = Resolve-Path -relative $_.FullName + + Write-Host "Uploading: $relativeFilePath" -ForegroundColor Yellow + & (& "$PSScriptRoot/Get-CodeCovTool.ps1") -t $CodeCovToken -f $relativeFilePath -R $RepoRoot -F $Flags -n $Name +} diff --git a/azure-pipelines/publish-codecoverage.yml b/azure-pipelines/publish-codecoverage.yml index 7be9459e5..7bf08b389 100644 --- a/azure-pipelines/publish-codecoverage.yml +++ b/azure-pipelines/publish-codecoverage.yml @@ -1,27 +1,17 @@ steps: - download: current artifact: coverageResults-Windows - displayName: Download Windows code coverage results + displayName: ๐Ÿ”ป Download Windows code coverage results continueOnError: true - download: current artifact: coverageResults-Linux - displayName: Download Linux code coverage results + displayName: ๐Ÿ”ป Download Linux code coverage results continueOnError: true -- powershell: | - dotnet tool install --tool-path obj dotnet-reportgenerator-globaltool --version 4.8.5 --configfile azure-pipelines/justnugetorg.nuget.config - Copy-Item -Recurse $(Pipeline.Workspace)/coverageResults-Windows/obj/* $(System.DefaultWorkingDirectory)/obj - Write-Host 'Substituting {reporoot} with $(System.DefaultWorkingDirectory)' - $reports = Get-ChildItem -Recurse '$(Pipeline.Workspace)/coverage.*cobertura.xml' - $reports |% { - $content = Get-Content -Path $_ |% { $_.Replace('{reporoot}', '$(System.DefaultWorkingDirectory)') } - Set-Content -Path $_ -Value $content -Encoding UTF8 - } - $Inputs = [string]::join(';', ($reports |% { Resolve-Path -relative $_ })) - obj/reportgenerator -reports:"$Inputs" -targetdir:coveragereport -reporttypes:Cobertura - displayName: Merge coverage +- powershell: azure-pipelines/Merge-CodeCoverage.ps1 -Path '$(Pipeline.Workspace)' -OutputDir coveragereport -Format Cobertura -Verbose + displayName: โš™ Merge coverage - task: PublishCodeCoverageResults@1 - displayName: Publish code coverage results to Azure DevOps + displayName: ๐Ÿ“ข Publish code coverage results to Azure DevOps inputs: codeCoverageTool: cobertura - summaryFileLocation: 'coveragereport/Cobertura.xml' + summaryFileLocation: coveragereport/Cobertura.xml failIfCoverageEmpty: true diff --git a/azure-pipelines/publish-deployables.yml b/azure-pipelines/publish-deployables.yml index 6ad5b38cd..6e5e38038 100644 --- a/azure-pipelines/publish-deployables.yml +++ b/azure-pipelines/publish-deployables.yml @@ -1,17 +1,17 @@ steps: - download: current - displayName: Download deployables + displayName: ๐Ÿ”ป Download deployables artifact: deployables-Windows - powershell: dotnet nuget push "$(Resolve-Path '$(Pipeline.Workspace)\deployables-Windows\')*.nupkg" -s $(ci_feed) -k azdo --skip-duplicate - displayName: Push packages to CI feed + displayName: ๐Ÿ“ฆ Push packages to CI feed condition: and(succeeded(), ne(variables['ci_feed'], ''), ne(variables['Build.Reason'], 'PullRequest')) - pwsh: Set-Content -Path "$(Agent.TempDirectory)/.npmrc" -Value "registry=$(ci_npm_feed)`nalways-auth=true" - displayName: Prepare to push to PublicCI + displayName: โš™๏ธ Prepare to push to PublicCI condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), ne(variables['Build.Reason'], 'PullRequest')) - task: npmAuthenticate@0 - displayName: Authenticate to PublicCI + displayName: ๐Ÿ” Authenticate to PublicCI inputs: workingFile: $(Agent.TempDirectory)/.npmrc condition: and(succeeded(), eq(variables['Agent.OS'], 'Windows_NT'), eq(variables['System.TeamFoundationCollectionUri'], 'https://dev.azure.com/andrewarnott/'), ne(variables['Build.Reason'], 'PullRequest')) @@ -20,6 +20,6 @@ steps: Write-Host "Will publish $tgz" npm publish $tgz workingDirectory: $(Agent.TempDirectory) - displayName: npm publish to PublicCI feed + displayName: ๐Ÿ“ฆ npm publish to PublicCI feed continueOnError: true condition: and(succeeded(), ne(variables['ci_npm_feed'], ''), ne(variables['Build.Reason'], 'PullRequest')) diff --git a/azure-pipelines/release.yml b/azure-pipelines/release.yml index ed95d940b..e89ac6453 100644 --- a/azure-pipelines/release.yml +++ b/azure-pipelines/release.yml @@ -25,18 +25,18 @@ jobs: } else { Write-Host "##vso[task.setvariable variable=IsPrerelease]false" } - displayName: Set up pipeline + displayName: โš™ Set up pipeline - task: UseDotNet@2 - displayName: Install .NET SDK + displayName: โš™ Install .NET SDK inputs: packageType: sdk version: 6.x - download: CI artifact: deployables-Windows - displayName: Download deployables-Windows artifact + displayName: ๐Ÿ”ป Download deployables-Windows artifact patterns: 'deployables-Windows/*' - task: GitHubRelease@1 - displayName: GitHub release (create) + displayName: ๐Ÿ“ข GitHub release (create) inputs: gitHubConnection: github.com_AArnott_OAuth repositoryName: $(Build.Repository.Name) @@ -51,12 +51,12 @@ jobs: changeLogType: issueBased changeLogLabels: | [ - { "label" : "breaking changes", "displayName" : "Breaking changes", "state" : "closed" }, + { "label" : "breaking change", "displayName" : "Breaking changes", "state" : "closed" }, { "label" : "bug", "displayName" : "Fixes", "state" : "closed" }, { "label" : "enhancement", "displayName": "Enhancements", "state" : "closed" } ] - script: dotnet nuget push $(Pipeline.Workspace)/CI/deployables-Windows/*.nupkg -s https://api.nuget.org/v3/index.json --api-key $(NuGetOrgApiKey) --skip-duplicate - displayName: Push packages to nuget.org + displayName: ๐Ÿ“ฆ Push packages to nuget.org condition: and(succeeded(), ne(variables['NuGetOrgApiKey'], '')) - powershell: | $tgz = (Get-ChildItem "$(Pipeline.Workspace)/CI/deployables-Windows/*.tgz")[0].FullName @@ -64,9 +64,9 @@ jobs: npm init -y npm install $tgz workingDirectory: $(Agent.TempDirectory) - displayName: Prepare to publish NPM package + displayName: โš™๏ธ Prepare to publish NPM package - task: Npm@1 - displayName: npm publish + displayName: ๐Ÿ“ฆ npm publish inputs: command: publish workingDir: $(Agent.TempDirectory)/node_modules/nerdbank-gitversioning diff --git a/azure-pipelines/test.runsettings b/azure-pipelines/test.runsettings new file mode 100644 index 000000000..c69022fc0 --- /dev/null +++ b/azure-pipelines/test.runsettings @@ -0,0 +1,44 @@ + + + + + + + + + \.dll$ + \.exe$ + + + xunit\..* + + + + + ^System\.Diagnostics\.DebuggerHiddenAttribute$ + ^System\.Diagnostics\.DebuggerNonUserCodeAttribute$ + ^System\.CodeDom\.Compiler\.GeneratedCodeAttribute$ + ^System\.Diagnostics\.CodeAnalysis\.ExcludeFromCodeCoverageAttribute$ + + + + + True + + True + + True + + False + + True + + True + + True + + + + + + diff --git a/azure-pipelines/variables/_all.ps1 b/azure-pipelines/variables/_all.ps1 index 0407d307e..cc6e88105 100644 --- a/azure-pipelines/variables/_all.ps1 +++ b/azure-pipelines/variables/_all.ps1 @@ -1,7 +1,14 @@ #!/usr/bin/env pwsh -# This script returns a hashtable of build variables that should be set -# at the start of a build or release definition's execution. +<# +.SYNOPSIS + This script returns a hashtable of build variables that should be set + at the start of a build or release definition's execution. +#> + +[CmdletBinding(SupportsShouldProcess = $true)] +param ( +) $vars = @{} diff --git a/azure-pipelines/variables/_pipelines.ps1 b/azure-pipelines/variables/_pipelines.ps1 index 867b7fc8b..951106d2d 100644 --- a/azure-pipelines/variables/_pipelines.ps1 +++ b/azure-pipelines/variables/_pipelines.ps1 @@ -1,8 +1,15 @@ -# This script translates the variables returned by the _all.ps1 script -# into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume. +<# +.SYNOPSIS + This script translates the variables returned by the _all.ps1 script + into commands that instruct Azure Pipelines to actually set those variables for other pipeline tasks to consume. -# The build or release definition may have set these variables to override -# what the build would do. So only set them if they have not already been set. + The build or release definition may have set these variables to override + what the build would do. So only set them if they have not already been set. +#> + +[CmdletBinding()] +param ( +) (& "$PSScriptRoot\_all.ps1").GetEnumerator() |% { # Always use ALL CAPS for env var names since Azure Pipelines converts variable names to all caps and on non-Windows OS, env vars are case sensitive. diff --git a/init.ps1 b/init.ps1 index 4d66f65bb..60a7010f2 100755 --- a/init.ps1 +++ b/init.ps1 @@ -33,8 +33,8 @@ #> [CmdletBinding(SupportsShouldProcess = $true)] Param ( - [ValidateSet('repo', 'machine')] - [string]$InstallLocality = 'repo', + [ValidateSet('repo', 'user', 'machine')] + [string]$InstallLocality = 'user', [Parameter()] [switch]$NoPrerequisites, [Parameter()] diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index 2faab3754..e7edee55a 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -1,10 +1,3 @@ - - cobertura - [xunit.*]* - - $(OutputPath)/ - - diff --git a/tools/Install-DotNetSdk.ps1 b/tools/Install-DotNetSdk.ps1 index 76a397f04..2bac3b9bc 100755 --- a/tools/Install-DotNetSdk.ps1 +++ b/tools/Install-DotNetSdk.ps1 @@ -80,7 +80,7 @@ Function Get-FileFromWeb([Uri]$Uri, $OutDir) { $OutFile = Join-Path $OutDir $Uri.Segments[-1] if (!(Test-Path $OutFile)) { Write-Verbose "Downloading $Uri..." - if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir } + if (!(Test-Path $OutDir)) { New-Item -ItemType Directory -Path $OutDir | Out-Null } try { (New-Object System.Net.WebClient).DownloadFile($Uri, $OutFile) } finally { @@ -91,35 +91,31 @@ Function Get-FileFromWeb([Uri]$Uri, $OutDir) { $OutFile } -Function Get-InstallerExe($Version, $Architecture, [switch]$Runtime) { - $sdkOrRuntime = 'Sdk' - if ($Runtime) { $sdkOrRuntime = 'Runtime' } - +Function Get-InstallerExe( + $Version, + $Architecture, + [ValidateSet('Sdk','Runtime','WindowsDesktop')] + [string]$sku +) { # Get the latest/actual version for the specified one $TypedVersion = [Version]$Version if ($TypedVersion.Build -eq -1) { - $versionInfo = -Split (Invoke-WebRequest -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sdkOrRuntime/$Version/latest.version" -UseBasicParsing) + $versionInfo = -Split (Invoke-WebRequest -Uri "https://dotnetcli.blob.core.windows.net/dotnet/$sku/$Version/latest.version" -UseBasicParsing) $Version = $versionInfo[-1] } $majorMinor = "$($TypedVersion.Major).$($TypedVersion.Minor)" $ReleasesFile = Join-Path $DotNetInstallScriptRoot "$majorMinor\releases.json" if (!(Test-Path $ReleasesFile)) { - Get-FileFromWeb -Uri "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/$majorMinor/releases.json" -OutDir (Split-Path $ReleasesFile) + Get-FileFromWeb -Uri "https://dotnetcli.blob.core.windows.net/dotnet/release-metadata/$majorMinor/releases.json" -OutDir (Split-Path $ReleasesFile) | Out-Null } $releases = Get-Content $ReleasesFile | ConvertFrom-Json $url = $null foreach ($release in $releases.releases) { $filesElement = $null - if ($Runtime) { - if ($release.runtime.version -eq $Version) { - $filesElement = $release.runtime.files - } - } else { - if ($release.sdk.version -eq $Version) { - $filesElement = $release.sdk.files - } + if ($release.$sku.version -eq $Version) { + $filesElement = $release.$sku.files } if ($filesElement) { @@ -139,15 +135,14 @@ Function Get-InstallerExe($Version, $Architecture, [switch]$Runtime) { if ($url) { Get-FileFromWeb -Uri $url -OutDir $DotNetInstallScriptRoot } else { - Write-Error "Unable to find release of $sdkOrRuntime v$Version" + Write-Error "Unable to find release of $sku v$Version" } } -Function Install-DotNet($Version, $Architecture, [switch]$Runtime) { - if ($Runtime) { $sdkSubstring = '' } else { $sdkSubstring = 'SDK ' } - Write-Host "Downloading .NET Core $sdkSubstring$Version..." - $Installer = Get-InstallerExe -Version $Version -Architecture $Architecture -Runtime:$Runtime - Write-Host "Installing .NET Core $sdkSubstring$Version..." +Function Install-DotNet($Version, $Architecture, [ValidateSet('Sdk','Runtime','WindowsDesktop')][string]$sku = 'Sdk') { + Write-Host "Downloading .NET Core $sku $Version..." + $Installer = Get-InstallerExe -Version $Version -Architecture $Architecture -sku $sku + Write-Host "Installing .NET Core $sku $Version..." cmd /c start /wait $Installer /install /passive /norestart if ($LASTEXITCODE -eq 3010) { Write-Verbose "Restart required" @@ -177,13 +172,25 @@ if ($InstallLocality -eq 'machine') { } } - $runtimeVersions | Get-Unique |% { - if ($PSCmdlet.ShouldProcess(".NET Core runtime $_", "Install")) { - Install-DotNet -Version $_ -Architecture $arch -Runtime + $runtimeVersions | Sort-Object | Get-Unique |% { + if ($PSCmdlet.ShouldProcess(".NET runtime $_", "Install")) { + Install-DotNet -Version $_ -sku Runtime -Architecture $arch + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + + if ($IncludeX86) { + Install-DotNet -Version $_ -sku Runtime -Architecture x86 + $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) + } + } + } + + $windowsDesktopRuntimeVersions | Sort-Object | Get-Unique |% { + if ($PSCmdlet.ShouldProcess(".NET Windows Desktop $_", "Install")) { + Install-DotNet -Version $_ -sku WindowsDesktop -Architecture $arch $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) if ($IncludeX86) { - Install-DotNet -Version $_ -Architecture x86 -Runtime + Install-DotNet -Version $_ -sku WindowsDesktop -Architecture x86 $restartRequired = $restartRequired -or ($LASTEXITCODE -eq 3010) } } diff --git a/tools/Install-NuGetCredProvider.ps1 b/tools/Install-NuGetCredProvider.ps1 index 6d3100349..496049a29 100644 --- a/tools/Install-NuGetCredProvider.ps1 +++ b/tools/Install-NuGetCredProvider.ps1 @@ -33,7 +33,7 @@ if ($IsMacOS -or $IsLinux) { $installerScript = Join-Path $toolsPath $installerScript -if (!(Test-Path $installerScript)) { +if (!(Test-Path $installerScript) -or $Force) { Invoke-WebRequest $sourceUrl -OutFile $installerScript } @@ -43,7 +43,7 @@ if ($IsMacOS -or $IsLinux) { chmod u+x $installerScript } -& $installerScript -Force:$Force +& $installerScript -Force:$Force -AddNetfx -InstallNet6 if ($AccessToken) { $endpoints = @()