diff --git a/.github/workflows/build-validation.yml b/.github/workflows/build-validation.yml new file mode 100644 index 0000000000..c8c4bd411a --- /dev/null +++ b/.github/workflows/build-validation.yml @@ -0,0 +1,81 @@ +# This is a basic workflow to help you get started with Actions +name: Snippets 5000 + +# Controls when the action will run. Triggers the workflow on push or pull request +# events but only for the master branch +on: + pull_request: + paths: + - "**.cs" + - "**.vb" + - "**.fs" + - "**.xaml" + - "**.razor" + - "**.cshtml" + - "**.vbhtml" + - "**.csproj" + - "**.vbproj" + - "**.fsproj" + - "**.sln" + - "**global.json" + - "**snippets.5000.json" + branches: [ master ] + +env: + DOTNET_INSTALLER_CHANNEL: 'release/5.0.1xx' + DOTNET_DO_INSTALL: 'false' + EnableNuGetPackageRestore: 'True' + +# A workflow run is made up of one or more jobs that can run sequentially or in parallel +jobs: + # This workflow contains a single job called "build" + build: + # The type of runner that the job will run on + runs-on: windows-latest + + # Steps represent a sequence of tasks that will be executed as part of the job + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@a81bbbf8298c0fa03ea29cdc473d45769f953675 #@v2 + + # Get the latest preview SDK (or sdk not installed by the runner) + - name: Setup .NET SDK + if: ${{ env.DOTNET_DO_INSTALL == 'true' }} + run: | + echo "Downloading dotnet-install.ps1" + Invoke-WebRequest https://raw.githubusercontent.com/dotnet/install-scripts/master/src/dotnet-install.ps1 -OutFile dotnet-install.ps1 + echo "Installing dotnet version ${{ env.DOTNET_INSTALLER_CHANNEL }}" + .\dotnet-install.ps1 -InstallDir "c:\program files\dotnet" -Channel "${{ env.DOTNET_INSTALLER_CHANNEL }}" + + # Print dotnet info + - name: Display .NET info + run: | + dotnet --info + + # Install locate projs global tool + - name: Install LocateProjects tool + run: | + dotnet tool install --global --add-source ./.github/workflows/dependencies/ DotnetDocsTools.LocateProjects + + # Run locate projs tool + - name: Locate projects for PR + env: + GitHubKey: ${{ secrets.GITHUB_TOKEN }} + LocateExts: ".cs;.vb;.fs;.cpp;.h;.xaml;.razor;.cshtml;.vbhtml;.csproj;.fsproj;.vbproj;.sln" + run: | + ./.github/workflows/dependencies/Get-MSBuildResults.ps1 "${{ github.workspace }}" -PullRequest ${{ github.event.number }} -RepoOwner ${{ github.repository_owner }} -RepoName ${{ github.event.repository.name }} + + # Update build output json file + - name: Upload build results + uses: actions/upload-artifact@3446296876d12d4e3a0f3145a3c87e67bf0a16b5 #@v1 + with: + name: build + path: ./output.json + + # Return status based on json file + - name: Report status + run: | + ./.github/workflows/dependencies/Out-GithubActionStatus.ps1 + + + diff --git a/.github/workflows/dependencies/DotnetDocsTools.LocateProjects.1.0.9.nupkg b/.github/workflows/dependencies/DotnetDocsTools.LocateProjects.1.0.9.nupkg new file mode 100644 index 0000000000..dd2da8d39d Binary files /dev/null and b/.github/workflows/dependencies/DotnetDocsTools.LocateProjects.1.0.9.nupkg differ diff --git a/.github/workflows/dependencies/Get-MSBuildResults.ps1 b/.github/workflows/dependencies/Get-MSBuildResults.ps1 new file mode 100644 index 0000000000..1ad66c55a5 --- /dev/null +++ b/.github/workflows/dependencies/Get-MSBuildResults.ps1 @@ -0,0 +1,288 @@ +<# + +.SYNOPSIS + Invokes dotnet build on the samples sln and project files. + +.DESCRIPTION + Invokes dotnet build on the samples sln and project files. + +.PARAMETER RepoRootDir + The directory of the repository files on the local machine. + +.PARAMETER PullRequest + The pull requst to process. If 0 or not passed, processes the whole repo + +.PARAMETER RepoOwner + The name of the repository owner. + +.PARAMETER RepoName + The name of the repository. + +.PARAMETER RangeStart + A range of results to process. + +.PARAMETER RangeEnd + A range of results to process. + +.INPUTS + None + +.OUTPUTS + None + +.NOTES + Version: 1.4 + Author: adegeo@microsoft.com + Creation Date: 12/11/2020 + Purpose/Change: Add support for config file. Select distinct on project files. +#> + +[CmdletBinding()] +Param( + [Parameter(Mandatory = $true, ValueFromPipeline = $false)] + [System.String] $RepoRootDir = $env:RepoRootDir, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [System.Int64] $PullRequest = 0, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [System.String] $RepoOwner = "", + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [System.String] $RepoName = "", + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [System.Int32] $RangeStart = $env:rangestart, + + [Parameter(Mandatory = $false, ValueFromPipeline = $false)] + [System.Int32] $RangeEnd = $env:rangeend +) + +$Global:statusOutput = @() + +Write-Host "Gathering solutions and projects..." + +if ($PullRequest -ne 0) { + Write-Host "Running `"LocateProjects `"$RepoRootDir`" --pullrequest $PullRequest --owner $RepoOwner --repo $RepoName`"" + $output = Invoke-Expression "LocateProjects `"$RepoRootDir`" --pullrequest $PullRequest --owner $RepoOwner --repo $RepoName" +} +else { + Write-Host "Running `"LocateProjects `"$RepoRootDir`"" + $output = Invoke-Expression "LocateProjects `"$RepoRootDir`"" +} + +if ($LASTEXITCODE -ne 0) +{ + $output + throw "Error on running LocateProjects" +} + +function New-Result($inputFile, $projectFile, $exitcode, $outputText) +{ + $info = @{} + + $info.InputFile = $inputFile + $info.ProjectFile = $projectFile + $info.ExitCode = $exitcode + $info.Output = $outputText + + $object = New-Object -TypeName PSObject -Prop $info + $Global:statusOutput += $object +} + +$workingSet = $output + +if (($RangeStart -ne 0) -and ($RangeEnd -ne 0)){ + $workingSet = $output[$RangeStart..$RangeEnd] +} + +# Log working set items prior to filtering +$workingSet | Write-Host + +# Remove duplicated projects +$projects = @() +$workingSetTemp = @() + +foreach ($item in $workingSet) { + $data = $item.Split('|') + if ($projects.Contains($data[2].Trim())) { + continue + } + if ($data[2].Trim() -ne "") { + $projects += $data[2].Trim() + } + $workingSetTemp += $item +} + +$workingSet = $workingSetTemp + +# Process working set +$counter = 1 +$length = $workingSet.Count +$thisExitCode = 0 + +$ErrorActionPreference = "Continue" + +foreach ($item in $workingSet) { + try { + Write-Host "$counter/$length :: $Item" + + $data = $item.Split('|') + + # Project found, build it + if ([int]$data[0] -eq 0) { + $projectFile = Resolve-Path "$RepoRootDir\$($data[2])" + $configFile = [System.IO.Path]::Combine([System.IO.Path]::GetDirectoryName($projectFile), "snippets.5000.json") + + # Create the default build command + "dotnet build `"$projectFile`"" | Out-File ".\run.bat" + + # Check for config file + if ([System.IO.File]::Exists($configFile) -eq $true) { + Write-Host "- Config file found" + + $settings = $configFile | Get-ChildItem | Get-Content | ConvertFrom-Json + + if ($settings.host -eq "visualstudio") { + Write-Host "- Using visual studio as build host" + + # Create the visual studio build command + "CALL `"C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\Common7\Tools\VsDevCmd.bat`"`n" + + "msbuild.exe `"$projectFile`" -restore:True" ` + | Out-File ".\run.bat" + } + elseif ($settings.host -eq "custom") { + Write-Host "- Using custom build host: $($settings.command)" + + $ExecutionContext.InvokeCommand.ExpandString($settings.command) | Out-File ".\run.bat" + } + elseif ($settings.host -eq "dotnet") { + Write-Host "- Using dotnet build host" + + "dotnet build `"$projectFile`"" | Out-File ".\run.bat" + } + else { + throw "snippets.5000.json file isn't valid." + } + } + + $result = Invoke-Expression ".\run.bat" | Out-String + $thisExitCode = 0 + + if ($LASTEXITCODE -ne 0) { + $thisExitCode = 4 + } + + New-Result $data[1] $projectFile $thisExitCode $result + } + + # No project found + elseif ([int]$data[0] -eq 1) { + New-Result $data[1] "" 1 "😵 Project missing. A project (and optionally a solution file) must be in this directory or one of the parent directories to validate and build this code." + + $thisExitCode = 1 + } + + # Too many projects found + elseif ([int]$data[0] -eq 2) { + New-Result $data[1] $data[2] 2 "😕 Too many projects found. A single project or solution must existing in this directory or one of the parent directories." + + $thisExitCode = 2 + } + + # Solution found, but no project + elseif ([int]$data[0] -eq 3) { + New-Result $data[1] $data[2] 2 "😲 Solution found, but missing project. A project is required to compile this code." + $thisExitCode = 3 + } + } + catch { + New-Result $data[1] $projectFile 1000 "ERROR: $($_.Exception)" + $thisExitCode = 4 + Write-Host $_.Exception.Message -Foreground "Red" + Write-Host $_.ScriptStackTrace -Foreground "DarkGray" + } + + $counter++ +} + +$resultItems = $Global:statusOutput | Select-Object InputFile, ProjectFile, ExitCode, Output + +# Add our output type +$typeResult = @" +public class ResultItem +{ + public string ProjectFile; + public string InputFile; + public int ExitCode; + public string BuildOutput; + public MSBuildError[] Errors; + public int ErrorCount; + + public class MSBuildError + { + public string Line; + public string Error; + } +} +"@ +Add-Type $typeResult + +$transformedItems = $resultItems | ForEach-Object { New-Object ResultItem -Property @{ + ProjectFile = $_.ProjectFile.Path; + InputFile = $_.InputFile; + ExitCode = $_.ExitCode; + BuildOutput = $_.Output; + Errors = @(); + ErrorCount = 0} + } + +# Transform the build output to break it down into MSBuild result entries +foreach ($item in $transformedItems) { + $list = @() + + # Clean + if ($item.ExitCode -eq 0) { + #$list += New-Object -TypeName "ResultItem+MSBuildError" -Property @{ Line = $item.BuildOutput; Error = $item.BuildOutput } + } + # No project found + # Too many projects found + # Solution found, but no project + elseif ($item.ExitCode -ne 4) { + $list += New-Object -TypeName "ResultItem+MSBuildError" -Property @{ Line = $item.BuildOutput; Error = $item.BuildOutput } + $item.ErrorCount = 1 + } + else { + $errorInfo = $item.BuildOutput -Split [System.Environment]::NewLine | + Select-String ": (?:Solution file error|error) ([^:]*)" | ` + Select-Object Line -ExpandProperty Matches | ` + Select-Object Line, Groups | ` + Sort-Object Line | Get-Unique -AsString + $item.ErrorCount = $errorInfo.Count + foreach ($err in $errorInfo) { + $list += New-Object -TypeName "ResultItem+MSBuildError" -Property @{ Line = $err.Line; Error = $err.Groups[1].Value } + } + + # Error count of 0 here means that no error was detected from build results, but there was still a failure of some kind + if ($item.ErrorCount -eq 0) { + $list += New-Object -TypeName "ResultItem+MSBuildError" -Property @{ Line = "Unknown error occurred. Check log and build output."; Error = "4" } + $item.ErrorCount = 1 + } + } + + $item.Errors = $list + +} + +$transformedItems | ConvertTo-Json -Depth 3 | Out-File 'output.json' + +exit 0 + + +# Sample snippets.5000.json file +<# +{ + "host": "visualstudio" +} + +#> diff --git a/.github/workflows/dependencies/Out-GithubActionStatus.ps1 b/.github/workflows/dependencies/Out-GithubActionStatus.ps1 new file mode 100644 index 0000000000..c0d4e3cab5 --- /dev/null +++ b/.github/workflows/dependencies/Out-GithubActionStatus.ps1 @@ -0,0 +1,53 @@ +<# + +.SYNOPSIS + Reads the output.json file and outputs status to GitHub Actions + +.DESCRIPTION + Reads the output.json file and outputs status to GitHub Actions + +.INPUTS + None + +.OUTPUTS + None + +.NOTES + Version: 1.1 + Author: adegeo@microsoft.com + Creation Date: 06/24/2020 + Purpose/Change: Change reporting items +#> + +[CmdletBinding()] +Param( +) + +$json = Get-Content output.json | ConvertFrom-Json + +$errors = $json | Where-Object ErrorCount -ne 0 | Select-Object InputFile -ExpandProperty Errors | Select-Object InputFile, Error, Line + +if ($errors.Count -eq 0) { + Write-Host "All builds passed" + exit 0 +} + +Write-Host "Total errors: $($errors.Count)" + +foreach ($er in $errors) { + + $lineColMatch = $er.Line | Select-String "(^.*)\((\d*),(\d*)\)" | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty Groups + $errorFile = $er.InputFile + $errorLineNumber = 0 + $errorColNumber = 0 + + if ($lineColMatch.Count -eq 4) { + $errorFile = $lineColMatch[1].Value.Replace("D:\a\docs\docs\", "").Replace("\", "/") + $errorLineNumber = $lineColMatch[2].Value + $errorColNumber = $lineColMatch[3].Value + } + + Write-Host "::error file=$errorFile,line=$errorLineNumber,col=$errorColNumber::$($er.Line)" +} + +exit 1