Skip to content

Instantly share code, notes, and snippets.

@tcartwright
Last active January 9, 2026 15:01
Show Gist options
  • Select an option

  • Save tcartwright/639aefbee39aa30044fec669c60082ce to your computer and use it in GitHub Desktop.

Select an option

Save tcartwright/639aefbee39aa30044fec669c60082ce to your computer and use it in GitHub Desktop.
AZURE YML: Windows On Premise Build Server Agent Report
trigger: none
variables:
cleanPoolName: ${{ replace(replace(replace(parameters.agentPool, '/', '_'), '\', '_'), ':', '_') }}
cleanAgentName: ${{ replace(replace(replace(replace(parameters.agentName, '*', 'ANY'), '/', '_'), '\', '_'), ':', '_') }}
reportFile: $(Build.ArtifactStagingDirectory)\BuildServerReport.txt
name: 'BuildServerReport_${{ variables.cleanPoolName }}_${{ variables.cleanAgentName }}'
parameters:
- name: agentPool
displayName: 'Select Agent Pool'
type: string
default: 'Default'
values:
# Add your on-premise pools below
- 'Default'
- name: agentName
displayName: 'Agent Name (optional, use star "*" for any)'
type: string
default: '*'
stages:
- stage: BuildServerReport
displayName: 'Report: ${{ parameters.agentPool }} / ${{ parameters.agentName }}'
jobs:
- job: CollectInfo
displayName: 'Collect Server Information'
pool:
name: ${{ parameters.agentPool }}
${{ if ne(parameters.agentName, '*') }}:
demands:
- Agent.Name -equals ${{ parameters.agentName }}
steps:
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
& {
$divider
"BUILD SERVER INFORMATION REPORT"
$divider
"{0,-25} {1}" -f "Generated:", (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
"{0,-25} {1}" -f "Pool:", '${{ parameters.agentPool }}'
"{0,-25} {1}" -f "Agent:", $env:AGENT_NAME
""
$subDivider
"REPORT PARAMETERS"
$subDivider
"{0,-25} {1}" -f "Pool:", '${{ parameters.agentPool }}'
"{0,-25} {1}" -f "Agent:", '${{ parameters.agentName }}'
""
} | Tee-Object -FilePath "$(reportFile)"
displayName: 'Report Header'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
& {
$divider
"OPERATING SYSTEM"
$divider
$os = Get-CimInstance Win32_OperatingSystem
$cs = Get-CimInstance Win32_ComputerSystem
$procs = Get-CimInstance Win32_Processor
"{0,-25} {1}" -f "Computer Name:", $os.CSName
"{0,-25} {1}" -f "OS Name:", $os.Caption
"{0,-25} {1}" -f "OS Version:", $os.Version
"{0,-25} {1}" -f "OS Build:", $os.BuildNumber
"{0,-25} {1}" -f "OS Architecture:", $os.OSArchitecture
"{0,-25} {1}" -f "Install Date:", $os.InstallDate
""
$subDivider
"HARDWARE"
$subDivider
"{0,-25} {1}" -f "Manufacturer:", $cs.Manufacturer
"{0,-25} {1}" -f "Model:", $cs.Model
"{0,-25} {1}" -f "Total RAM:", "$([math]::Round($cs.TotalPhysicalMemory / 1GB, 2)) GB"
"{0,-25} {1}" -f "Free RAM:", "$([math]::Round($os.FreePhysicalMemory / 1MB, 2)) GB"
"{0,-25} {1}" -f "Logical Processors:", $cs.NumberOfLogicalProcessors
""
$subDivider
"PROCESSORS"
$subDivider
$i = 1
foreach ($proc in $procs) {
"{0,-25} {1}" -f "Processor $i`:", $proc.Name
"{0,-25} {1}" -f " Cores:", $proc.NumberOfCores
"{0,-25} {1}" -f " Logical Processors:", $proc.NumberOfLogicalProcessors
"{0,-25} {1}" -f " Max Clock Speed:", "$($proc.MaxClockSpeed) MHz"
$i++
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'OS Information'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
$output = & {
$divider
"SYSTEM UPTIME"
$divider
$os = Get-CimInstance Win32_OperatingSystem
$lastBoot = $os.LastBootUpTime
$uptime = (Get-Date) - $lastBoot
"{0,-25} {1:yyyy-MM-dd HH:mm:ss}" -f "Last Reboot:", $lastBoot
"{0,-25} {1} days, {2} hours, {3} minutes" -f "Uptime:", $uptime.Days, $uptime.Hours, $uptime.Minutes
""
if ($uptime.Days -gt 30) {
"WARNING: Server has not been rebooted in over 30 days ($($uptime.Days) days)"
}
""
}
$output | Tee-Object -FilePath "$(reportFile)" -Append
# Azure warning separately so it shows in pipeline UI
$os = Get-CimInstance Win32_OperatingSystem
$uptime = (Get-Date) - $os.LastBootUpTime
if ($uptime.Days -gt 30) {
Write-Host "##[warning]Server has not been rebooted in over 30 days ($($uptime.Days) days)"
}
displayName: 'System Uptime'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
& {
$divider
"SERVICE PACKS & HOTFIXES"
$divider
$os = Get-CimInstance Win32_OperatingSystem
"{0,-25} {1}" -f "Service Pack:", $(if ($os.ServicePackMajorVersion -gt 0) { "SP$($os.ServicePackMajorVersion)" } else { "None" })
"{0,-25} {1}" -f "OS Build:", $os.BuildNumber
"{0,-25} {1}" -f "OS Version:", $os.Version
""
$ubr = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).UBR
if ($ubr) {
"{0,-25} {1}.{2}" -f "Full Build:", $os.BuildNumber, $ubr
}
$releaseId = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).ReleaseId
$displayVersion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).DisplayVersion
if ($displayVersion) {
"{0,-25} {1}" -f "Display Version:", $displayVersion
} elseif ($releaseId) {
"{0,-25} {1}" -f "Release ID:", $releaseId
}
""
$subDivider
"INSTALLED HOTFIXES"
$subDivider
$hotfixes = Get-HotFix -ErrorAction SilentlyContinue |
Sort-Object { if ($_.InstalledOn) { $_.InstalledOn } else { [DateTime]::MinValue } } -Descending
"{0,-25} {1}" -f "Total Hotfixes:", $hotfixes.Count
""
"{0,-15} {1,-40} {2}" -f "HotFixID", "Description", "Installed On"
"{0,-15} {1,-40} {2}" -f "--------", "-----------", "------------"
foreach ($hf in $hotfixes | Select-Object -First 25) {
$installedDate = if ($hf.InstalledOn) { $hf.InstalledOn.ToString("yyyy-MM-dd") } else { "Unknown" }
"{0,-15} {1,-40} {2}" -f $hf.HotFixID, $hf.Description, $installedDate
}
if ($hotfixes.Count -gt 25) {
""
"... and $($hotfixes.Count - 25) more (showing most recent 25)"
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'Service Packs & Hotfixes'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
& {
$divider
"NETWORK / IP INFORMATION"
$divider
"{0,-25} {1}" -f "Hostname:", $env:COMPUTERNAME
"{0,-25} {1}" -f "FQDN:", ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName)
""
$subDivider
"NETWORK ADAPTERS"
$subDivider
$adapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' }
foreach ($adapter in $adapters) {
""
"{0,-25} {1}" -f "Adapter:", $adapter.Name
"{0,-25} {1}" -f " Description:", $adapter.InterfaceDescription
"{0,-25} {1}" -f " MAC Address:", $adapter.MacAddress
"{0,-25} {1}" -f " Link Speed:", $adapter.LinkSpeed
"{0,-25} {1}" -f " Status:", $adapter.Status
$ipConfig = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -ErrorAction SilentlyContinue
$ipv4 = $ipConfig | Where-Object { $_.AddressFamily -eq 'IPv4' }
$ipv6 = $ipConfig | Where-Object { $_.AddressFamily -eq 'IPv6' }
foreach ($ip in $ipv4) {
"{0,-25} {1}/{2}" -f " IPv4 Address:", $ip.IPAddress, $ip.PrefixLength
}
foreach ($ip in $ipv6) {
if ($ip.IPAddress -notlike 'fe80*') {
"{0,-25} {1}/{2}" -f " IPv6 Address:", $ip.IPAddress, $ip.PrefixLength
}
}
$gateway = Get-NetRoute -InterfaceIndex $adapter.ifIndex -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue
if ($gateway) {
"{0,-25} {1}" -f " Default Gateway:", $gateway.NextHop
}
}
""
$subDivider
"DNS CONFIGURATION"
$subDivider
$dnsServers = Get-DnsClientServerAddress | Where-Object { $_.ServerAddresses -and $_.AddressFamily -eq 2 }
$uniqueDns = $dnsServers.ServerAddresses | Select-Object -Unique
$i = 1
foreach ($dns in $uniqueDns) {
"{0,-25} {1}" -f "DNS Server $i`:", $dns
$i++
}
""
$subDivider
"IP ADDRESS SUMMARY"
$subDivider
"{0,-20} {1,-18} {2,-10} {3}" -f "Adapter", "IP Address", "Type", "Prefix"
"{0,-20} {1,-18} {2,-10} {3}" -f "-------", "----------", "----", "------"
Get-NetIPAddress | Where-Object {
$_.AddressState -ieq 'Preferred' -and
$_.IPAddress -notlike '127.*' -and
$_.IPAddress -inotlike 'fe80*' -and
$_.IPAddress -ne '::1'
} | ForEach-Object {
$adapterName = (Get-NetAdapter -InterfaceIndex $_.InterfaceIndex -ErrorAction SilentlyContinue).Name
if (-not $adapterName) { $adapterName = "Unknown" }
$adapterName = if ($adapterName.Length -gt 18) { $adapterName.Substring(0,15) + "..." } else { $adapterName }
"{0,-20} {1,-18} {2,-10} {3}" -f $adapterName, $_.IPAddress, $_.AddressFamily, "/$($_.PrefixLength)"
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'Network / IP Information'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$warnings = @()
$output = & {
$divider
"AZURE & DEVOPS CONNECTIVITY"
$divider
$endpoints = @(
@{Name="Azure DevOps"; Url="https://dev.azure.com"; Port=443},
@{Name="Azure DevOps (vsrm)"; Url="https://vsrm.dev.azure.com"; Port=443},
@{Name="Azure Artifacts"; Url="https://pkgs.dev.azure.com"; Port=443},
@{Name="Azure Portal"; Url="https://portal.azure.com"; Port=443},
@{Name="Azure Management API"; Url="https://management.azure.com"; Port=443},
@{Name="Azure AD / Entra"; Url="https://login.microsoftonline.com"; Port=443},
@{Name="Azure Blob Storage"; Url="https://blob.core.windows.net"; Port=443},
@{Name="Azure Key Vault"; Url="https://vault.azure.net"; Port=443},
@{Name="NuGet Gallery"; Url="https://api.nuget.org"; Port=443},
@{Name="GitHub"; Url="https://github.com"; Port=443},
@{Name="VS Marketplace"; Url="https://marketplace.visualstudio.com"; Port=443}
)
"{0,-30} {1,-12} {2,-10} {3}" -f "Endpoint", "Status", "Time (ms)", "Details"
"{0,-30} {1,-12} {2,-10} {3}" -f "--------", "------", "---------", "-------"
foreach ($ep in $endpoints) {
$uri = [System.Uri]$ep.Url
$host_ = $uri.Host
try {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$tcp = New-Object System.Net.Sockets.TcpClient
$connect = $tcp.BeginConnect($host_, $ep.Port, $null, $null)
$wait = $connect.AsyncWaitHandle.WaitOne(5000, $false)
$sw.Stop()
if ($wait -and $tcp.Connected) {
$tcp.EndConnect($connect)
$tcp.Close()
"{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "OK", $sw.ElapsedMilliseconds, $host_
} else {
$tcp.Close()
"{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "TIMEOUT", "-", $host_
$script:warnings += "Connection timeout to $($ep.Name) ($host_)"
}
} catch {
"{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "FAILED", "-", $_.Exception.Message
$script:warnings += "Failed to connect to $($ep.Name) ($host_`:$($ep.Port)): $($_.Exception.Message)"
}
}
""
$subDivider
"HTTPS ENDPOINT VALIDATION"
$subDivider
"{0,-30} {1,-12} {2,-10} {3}" -f "Endpoint", "Status", "HTTP Code", "Details"
"{0,-30} {1,-12} {2,-10} {3}" -f "--------", "------", "---------", "-------"
$httpEndpoints = @(
@{Name="Azure DevOps API"; Url="https://dev.azure.com"},
@{Name="Azure Status"; Url="https://status.azure.com/en-us/status"},
@{Name="NuGet API"; Url="https://api.nuget.org/v3/index.json"},
@{Name="Azure Pipelines Agent"; Url="https://download.agent.dev.azure.com"}
)
foreach ($ep in $httpEndpoints) {
try {
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$response = Invoke-WebRequest -Uri $ep.Url -Method Get -UseBasicParsing -TimeoutSec 10 -MaximumRedirection 5 -ErrorAction Stop
$sw.Stop()
"{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "OK", $response.StatusCode, "$($sw.ElapsedMilliseconds)ms"
} catch {
$statusCode = $null
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
}
if ($statusCode) {
if ($statusCode -in 400, 401, 403, 404) {
"{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "Reachable", $statusCode, "$($statusCode) (auth/not found - expected)"
} else {
"{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "HTTP Error", $statusCode, $_.Exception.Message
$script:warnings += "HTTP request failed for $($ep.Name) ($($ep.Url)) - Status: $statusCode"
}
} else {
"{0,-30} {1,-12} {2,-10} {3}" -f $ep.Name, "FAILED", "-", $_.Exception.Message
$script:warnings += "Request failed for $($ep.Name) ($($ep.Url)): $($_.Exception.Message)"
}
}
}
""
$subDivider
"DNS RESOLUTION TEST"
$subDivider
"{0,-35} {1,-12} {2}" -f "Hostname", "Status", "Resolved IP(s)"
"{0,-35} {1,-12} {2}" -f "--------", "------", "--------------"
$dnsHosts = @(
"dev.azure.com",
"management.azure.com",
"login.microsoftonline.com",
"api.nuget.org",
"github.com"
)
foreach ($h in $dnsHosts) {
try {
$resolved = [System.Net.Dns]::GetHostAddresses($h)
$ips = ($resolved | Select-Object -First 3 | ForEach-Object { $_.IPAddressToString }) -join ", "
if ($resolved.Count -gt 3) { $ips += " ..." }
"{0,-35} {1,-12} {2}" -f $h, "OK", $ips
} catch {
"{0,-35} {1,-12} {2}" -f $h, "FAILED", $_.Exception.Message
$script:warnings += "DNS resolution failed for $h"
}
}
""
}
$output | Tee-Object -FilePath "$(reportFile)" -Append
# Emit Azure warnings separately
foreach ($w in $warnings) {
Write-Host "##[warning]$w"
}
displayName: 'Azure & DevOps Connectivity'
- powershell: |
$divider = "=" * 80
$warnings = @()
$output = & {
$divider
"DISK SPACE"
$divider
"{0,-10} {1,15} {2,15} {3,15} {4,10}" -f "Drive", "Total (GB)", "Used (GB)", "Free (GB)", "Free %"
"{0,-10} {1,15} {2,15} {3,15} {4,10}" -f "-----", "----------", "---------", "---------", "------"
Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
$total = [math]::Round($_.Size / 1GB, 2)
$free = [math]::Round($_.FreeSpace / 1GB, 2)
$used = [math]::Round($total - $free, 2)
$pct = [math]::Round(($free / $total) * 100, 1)
"{0,-10} {1,15} {2,15} {3,15} {4,10}" -f $_.DeviceID, $total, $used, $free, "$pct%"
if ($pct -lt 10) {
$script:warnings += "Low disk space on $($_.DeviceID) - only $pct% free"
}
}
""
}
$output | Tee-Object -FilePath "$(reportFile)" -Append
foreach ($w in $warnings) {
Write-Host "##[warning]$w"
}
displayName: 'Disk Space'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
& {
$divider
".NET FRAMEWORK"
$divider
"{0,-30} {1,-20} {2}" -f "Component", "Version", "Release"
"{0,-30} {1,-20} {2}" -f "---------", "-------", "-------"
$netFwPath = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP'
if (Test-Path $netFwPath) {
Get-ChildItem $netFwPath -Recurse | Get-ItemProperty -Name Version, Release -ErrorAction SilentlyContinue |
Where-Object { $_.PSChildName -match '^(?!S)\p{L}'} |
Select-Object @{n='Component';e={$_.PSChildName}}, Version, Release |
Sort-Object Component -Unique |
ForEach-Object {
$rel = if ($_.Release) { $_.Release } else { "-" }
"{0,-30} {1,-20} {2}" -f $_.Component, $_.Version, $rel
}
}
""
$subDivider
".NET FRAMEWORK 4.5+ DETECTION"
$subDivider
$release = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -ErrorAction SilentlyContinue).Release
if ($release) {
$version = switch ($release) {
{ $_ -ge 533320 } { "4.8.1 or later"; break }
{ $_ -ge 528040 } { "4.8"; break }
{ $_ -ge 461808 } { "4.7.2"; break }
{ $_ -ge 461308 } { "4.7.1"; break }
{ $_ -ge 460798 } { "4.7"; break }
{ $_ -ge 394802 } { "4.6.2"; break }
{ $_ -ge 394254 } { "4.6.1"; break }
{ $_ -ge 393295 } { "4.6"; break }
{ $_ -ge 379893 } { "4.5.2"; break }
{ $_ -ge 378675 } { "4.5.1"; break }
{ $_ -ge 378389 } { "4.5"; break }
default { "Unknown" }
}
"{0,-25} {1}" -f "Detected Version:", $version
"{0,-25} {1}" -f "Release Number:", $release
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: '.NET Framework Versions'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
& {
$divider
".NET CORE / .NET SDKs"
$divider
$dotnetPath = Get-Command dotnet -ErrorAction SilentlyContinue
if ($dotnetPath) {
"{0,-25} {1}" -f "CLI Location:", $dotnetPath.Source
""
$subDivider
"INSTALLED SDKs"
$subDivider
"{0,-20} {1}" -f "Version", "Location"
"{0,-20} {1}" -f "-------", "--------"
dotnet --list-sdks | ForEach-Object {
if ($_ -match '^(\S+)\s+\[(.+)\]$') {
"{0,-20} {1}" -f $matches[1], $matches[2]
}
}
""
$subDivider
"INSTALLED RUNTIMES"
$subDivider
"{0,-35} {1,-15} {2}" -f "Runtime", "Version", "Location"
"{0,-35} {1,-15} {2}" -f "-------", "-------", "--------"
dotnet --list-runtimes | ForEach-Object {
if ($_ -match '^(\S+)\s+(\S+)\s+\[(.+)\]$') {
"{0,-35} {1,-15} {2}" -f $matches[1], $matches[2], $matches[3]
}
}
} else {
"dotnet CLI not found in PATH"
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: '.NET Core/SDK Information'
- powershell: |
$divider = "=" * 80
& {
$divider
"INSTALLED APPLICATIONS"
$divider
"{0,-50} {1,-20} {2}" -f "Name", "Version", "Publisher"
"{0,-50} {1,-20} {2}" -f "----", "-------", "---------"
$apps = @()
$regPaths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($path in $regPaths) {
if (Test-Path $path) {
$apps += Get-ItemProperty $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and $_.DisplayName.Trim() } |
Select-Object DisplayName, DisplayVersion, Publisher
}
}
$apps | Sort-Object DisplayName -Unique | ForEach-Object {
$name = if ($_.DisplayName.Length -gt 48) { $_.DisplayName.Substring(0,45) + "..." } else { $_.DisplayName }
$ver = if ($_.DisplayVersion) { $_.DisplayVersion } else { "-" }
$ver = if ($ver.Length -gt 18) { $ver.Substring(0,15) + "..." } else { $ver }
$pub = if ($_.Publisher) { $_.Publisher } else { "-" }
$pub = if ($pub.Length -gt 30) { $pub.Substring(0,27) + "..." } else { $pub }
"{0,-50} {1,-20} {2}" -f $name, $ver, $pub
}
""
"{0,-25} {1}" -f "Total Applications:", $apps.Count
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'Installed Applications'
- powershell: |
$ErrorActionPreference = 'Continue'
$divider = "=" * 80
& {
$divider
"DEVELOPMENT TOOLS"
$divider
"{0,-20} {1,-15} {2}" -f "Tool", "Status", "Version"
"{0,-20} {1,-15} {2}" -f "----", "------", "-------"
$tools = @(
@{Name="Git"; Cmd="git"; Args=@("--version")},
@{Name="Node.js"; Cmd="node"; Args=@("--version")},
@{Name="npm"; Cmd="npm"; Args=@("--version")},
@{Name="Python"; Cmd="python"; Args=@("--version")},
@{Name="Java"; Cmd="java"; Args=@("-version")},
@{Name="Maven"; Cmd="mvn"; Args=@("--version")},
@{Name="NuGet"; Cmd="nuget"; Args=@("help")},
@{Name="Azure CLI"; Cmd="az"; Args=@("--version")},
@{Name="PowerShell"; Cmd="powershell"; Args=@("-Command", "`$PSVersionTable.PSVersion.ToString()")}
@{Name="PowerShell Core"; Cmd="pwsh"; Args=@("--version")},
@{Name="Docker"; Cmd="docker"; Args=@("--version")}
)
foreach ($tool in $tools) {
$cmds = @(Get-Command $tool.Cmd -ErrorAction SilentlyContinue)
$versions = @()
foreach ($cmd in $cmds) {
try {
$output = & $cmd.Source $tool.Args 2>&1
$outStr = $output | Out-String
if ($LASTEXITCODE -eq 0 -and $outStr -inotmatch "was not found|Microsoft Store") {
$ver = ($outStr.Split("`n") | Where-Object { $_.Trim() } | Select-Object -First 1).Trim()
$versions += @{Version=$ver; Path=$cmd.Source}
}
} catch {
continue
}
}
if ($versions.Count -eq 0) {
"{0,-20} {1,-15} {2}" -f $tool.Name, "Not Installed", "-"
} elseif ($versions.Count -eq 1) {
"{0,-20} {1,-15} {2}" -f $tool.Name, "Installed", $versions[0].Version
} else {
"{0,-20} {1,-15} {2}" -f $tool.Name, "Installed", "$($versions.Count) versions"
foreach ($v in $versions) {
"{0,-20} {1,-15} {2}" -f "", "", $v.Version
"{0,-20} {1,-15} -> {2}" -f "", "", $v.Path
}
}
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'Development Tools'
continueOnError: true
- powershell: |
$divider = "=" * 80
& {
$divider
"VISUAL STUDIO INSTALLATIONS"
$divider
$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vsWhere) {
"{0,-25} {1,-20} {2,-15} {3}" -f "Product", "Version", "Channel", "Path"
"{0,-25} {1,-20} {2,-15} {3}" -f "-------", "-------", "-------", "----"
$instances = & $vsWhere -all -format json | ConvertFrom-Json
foreach ($vs in $instances) {
$channel = ($vs.channelId -split '\.')[-1]
"{0,-25} {1,-20} {2,-15} {3}" -f $vs.displayName, $vs.installationVersion, $channel, $vs.installationPath
}
} else {
"vswhere.exe not found - Visual Studio may not be installed"
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'Visual Studio Info'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
& {
$divider
"MSBUILD INSTALLATIONS"
$divider
"{0,-15} {1,-25} {2}" -f "Version", "VS Product", "Path"
"{0,-15} {1,-25} {2}" -f "-------", "----------", "----"
$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vsWhere) {
$msbuildPaths = & $vsWhere -all -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe
foreach ($msbuildPath in $msbuildPaths) {
if (Test-Path $msbuildPath) {
$version = (Get-Item $msbuildPath).VersionInfo.ProductVersion
$vsProduct = if ($msbuildPath -match 'Visual Studio\\(\d{4})\\(\w+)\\') { "$($matches[1]) $($matches[2])" } else { "-" }
"{0,-15} {1,-25} {2}" -f $version, $vsProduct, $msbuildPath
}
}
}
$buildToolsPaths = @(
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe",
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe",
"${env:ProgramFiles(x86)}\MSBuild\14.0\Bin\MSBuild.exe",
"${env:ProgramFiles(x86)}\MSBuild\12.0\Bin\MSBuild.exe"
)
""
$subDivider
"STANDALONE / LEGACY MSBUILD"
$subDivider
"{0,-15} {1,-25} {2}" -f "Version", "Type", "Path"
"{0,-15} {1,-25} {2}" -f "-------", "----", "----"
foreach ($path in $buildToolsPaths) {
if (Test-Path $path) {
$version = (Get-Item $path).VersionInfo.ProductVersion
$type = if ($path -match 'BuildTools') { "Build Tools" } else { "Standalone" }
"{0,-15} {1,-25} {2}" -f $version, $type, $path
}
}
""
$subDivider
"MSBUILD IN PATH"
$subDivider
$msbuildInPath = Get-Command msbuild -ErrorAction SilentlyContinue
if ($msbuildInPath) {
$version = & msbuild -version | Select-Object -Last 1
"{0,-25} {1}" -f "Version:", $version
"{0,-25} {1}" -f "Location:", $msbuildInPath.Source
} else {
"MSBuild not found in PATH"
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'MSBuild Information'
- powershell: |
$divider = "=" * 80
$subDivider = "-" * 60
& {
$divider
"BUILD AGENT IDENTITY"
$divider
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
"{0,-25} {1}" -f "Username:", $identity.Name
"{0,-25} {1}" -f "User SID:", $identity.User.Value
"{0,-25} {1}" -f "Auth Type:", $identity.AuthenticationType
"{0,-25} {1}" -f "Is System:", $identity.IsSystem
"{0,-25} {1}" -f "Is Admin:", $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
""
$subDivider
"GROUP MEMBERSHIPS"
$subDivider
$identity.Groups | ForEach-Object {
try {
$group = $_.Translate([System.Security.Principal.NTAccount])
" - {0}" -f $group.Value
} catch {
" - {0} (unresolved)" -f $_.Value
}
}
""
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'Build Agent Identity'
- powershell: |
$divider = "=" * 80
& {
$divider
"AZURE DEVOPS AGENT VARIABLES"
$divider
"{0,-40} {1}" -f "Variable", "Value"
"{0,-40} {1}" -f "--------", "-----"
Get-ChildItem env: | Where-Object {
$_.Name -like 'AGENT_*'
} | Sort-Object Name | ForEach-Object {
"{0,-40} {1}" -f $_.Name, $_.Value
}
""
$divider
"BUILD VARIABLES"
$divider
"{0,-40} {1}" -f "Variable", "Value"
"{0,-40} {1}" -f "--------", "-----"
Get-ChildItem env: | Where-Object {
$_.Name -like 'BUILD_*'
} | Sort-Object Name | ForEach-Object {
"{0,-40} {1}" -f $_.Name, $_.Value
}
""
$divider
"END OF REPORT"
$divider
} | Tee-Object -FilePath "$(reportFile)" -Append
displayName: 'Environment Variables'
- task: PublishPipelineArtifact@1
inputs:
targetPath: '$(reportFile)'
artifact: 'BuildServerReport_${{ variables.cleanPoolName }}_${{ variables.cleanAgentName }}'
publishLocation: 'pipeline'
displayName: 'Publish Report Artifact'
#Requires -Version 5.1
<#
.SYNOPSIS
Generates a comprehensive server information report in JSON format.
.DESCRIPTION
Collects system information including OS, hardware, network, installed software,
and development tools. Outputs JSON to console and optionally to a file.
.PARAMETER OutputPath
Path to save the JSON report file. If not specified, outputs to console only.
.PARAMETER ComputerName
Remote computer to run against. If not specified, runs locally.
.PARAMETER Credential
Credentials for remote computer. Only used with -ComputerName.
.PARAMETER Compress
Output minified JSON instead of formatted.
.EXAMPLE
.\ServerReport-Json.ps1
.\ServerReport-Json.ps1 -OutputPath "C:\Reports\ServerReport.json"
.\ServerReport-Json.ps1 -ComputerName "SERVER01" -Credential (Get-Credential)
#>
param(
[string]$OutputPath = "$($env:TEMP)\ServerReport.txt",
[string]$ComputerName,
[PSCredential]$Credential,
[switch]$Compress
)
Clear-Host
$ErrorActionPreference = 'Continue'
# Build remote session params if needed
$remoteParams = @{}
if ($ComputerName) {
$remoteParams['ComputerName'] = $ComputerName
if ($Credential) {
$remoteParams['Credential'] = $Credential
}
Write-Host "Connecting to remote computer: $ComputerName" -ForegroundColor Cyan
}
# Scriptblock containing all the report logic
$reportScript = {
$report = [ordered]@{}
#region Report Metadata
$report.Metadata = [ordered]@{
GeneratedAt = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ss')
ServerName = $env:COMPUTERNAME
ReportVersion = "1.0"
}
#endregion
#region OS Information
$os = Get-CimInstance Win32_OperatingSystem
$cs = Get-CimInstance Win32_ComputerSystem
$report.OperatingSystem = [ordered]@{
ComputerName = $os.CSName
Name = $os.Caption
Version = $os.Version
BuildNumber = $os.BuildNumber
Architecture = $os.OSArchitecture
InstallDate = $os.InstallDate.ToString('yyyy-MM-ddTHH:mm:ss')
}
# Add UBR and display version
$ubr = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).UBR
if ($ubr) {
$report.OperatingSystem.FullBuild = "$($os.BuildNumber).$ubr"
}
$displayVersion = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion" -ErrorAction SilentlyContinue).DisplayVersion
if ($displayVersion) {
$report.OperatingSystem.DisplayVersion = $displayVersion
}
#endregion
#region Hardware
$procs = Get-CimInstance Win32_Processor
$report.Hardware = [ordered]@{
Manufacturer = $cs.Manufacturer
Model = $cs.Model
TotalRamGB = [math]::Round($cs.TotalPhysicalMemory / 1GB, 2)
FreeRamGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2)
LogicalProcessors = $cs.NumberOfLogicalProcessors
Processors = @($procs | ForEach-Object {
[ordered]@{
Name = $_.Name
Cores = $_.NumberOfCores
LogicalProcessors = $_.NumberOfLogicalProcessors
MaxClockSpeedMHz = $_.MaxClockSpeed
}
})
}
#endregion
#region System Uptime
$lastBoot = $os.LastBootUpTime
$uptime = (Get-Date) - $lastBoot
$report.Uptime = [ordered]@{
LastReboot = $lastBoot.ToString('yyyy-MM-ddTHH:mm:ss')
UptimeDays = $uptime.Days
UptimeHours = $uptime.Hours
UptimeMinutes = $uptime.Minutes
UptimeTotalHours = [math]::Round($uptime.TotalHours, 2)
Warning = if ($uptime.Days -gt 30) { "Server has not been rebooted in over 30 days" } else { $null }
}
#endregion
#region Hotfixes
$hotfixes = Get-HotFix -ErrorAction SilentlyContinue |
Sort-Object { if ($_.InstalledOn) { $_.InstalledOn } else { [DateTime]::MinValue } } -Descending
$report.Hotfixes = [ordered]@{
TotalCount = $hotfixes.Count
Items = @($hotfixes | Select-Object -First 50 | ForEach-Object {
[ordered]@{
HotFixID = $_.HotFixID
Description = $_.Description
InstalledOn = if ($_.InstalledOn) { $_.InstalledOn.ToString('yyyy-MM-dd') } else { $null }
InstalledBy = $_.InstalledBy
}
})
}
#endregion
#region Network
$adapters = Get-NetAdapter -ErrorAction SilentlyContinue | Where-Object { $_.Status -eq 'Up' }
$networkAdapters = @($adapters | ForEach-Object {
$adapter = $_
$ipConfig = Get-NetIPAddress -InterfaceIndex $adapter.ifIndex -ErrorAction SilentlyContinue
$gateway = Get-NetRoute -InterfaceIndex $adapter.ifIndex -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue
[ordered]@{
Name = $adapter.Name
Description = $adapter.InterfaceDescription
MacAddress = $adapter.MacAddress
LinkSpeed = $adapter.LinkSpeed
Status = $adapter.Status
IPv4Addresses = @($ipConfig | Where-Object { $_.AddressFamily -eq 'IPv4' } | ForEach-Object {
"$($_.IPAddress)/$($_.PrefixLength)"
})
IPv6Addresses = @($ipConfig | Where-Object { $_.AddressFamily -eq 'IPv6' -and $_.IPAddress -notlike 'fe80*' } | ForEach-Object {
"$($_.IPAddress)/$($_.PrefixLength)"
})
DefaultGateway = if ($gateway) { $gateway.NextHop } else { $null }
}
})
$dnsServers = Get-DnsClientServerAddress -ErrorAction SilentlyContinue |
Where-Object { $_.ServerAddresses -and $_.AddressFamily -eq 2 }
$uniqueDns = @($dnsServers.ServerAddresses | Select-Object -Unique)
$fqdn = try { ([System.Net.Dns]::GetHostEntry($env:COMPUTERNAME).HostName) } catch { $null }
$report.Network = [ordered]@{
Hostname = $env:COMPUTERNAME
FQDN = $fqdn
DnsServers = $uniqueDns
Adapters = $networkAdapters
}
#endregion
#region Disk Space
$disks = Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3"
$report.Disks = @($disks | ForEach-Object {
$total = [math]::Round($_.Size / 1GB, 2)
$free = [math]::Round($_.FreeSpace / 1GB, 2)
$used = [math]::Round($total - $free, 2)
$pct = [math]::Round(($free / $total) * 100, 1)
[ordered]@{
Drive = $_.DeviceID
VolumeName = $_.VolumeName
TotalGB = $total
UsedGB = $used
FreeGB = $free
FreePercent = $pct
Warning = if ($pct -lt 10) { "Low disk space" } else { $null }
}
})
#endregion
#region .NET Framework
$netFwPath = 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP'
$frameworks = @()
if (Test-Path $netFwPath) {
$frameworks = @(Get-ChildItem $netFwPath -Recurse |
Get-ItemProperty -Name Version, Release -ErrorAction SilentlyContinue |
Where-Object { $_.PSChildName -match '^(?!S)\p{L}'} |
Select-Object @{n='Component';e={$_.PSChildName}}, Version, Release |
Sort-Object Component -Unique |
ForEach-Object {
[ordered]@{
Component = $_.Component
Version = $_.Version
Release = $_.Release
}
})
}
$net4Release = (Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full" -ErrorAction SilentlyContinue).Release
$net4Version = if ($net4Release) {
switch ($net4Release) {
{ $_ -ge 533320 } { "4.8.1 or later"; break }
{ $_ -ge 528040 } { "4.8"; break }
{ $_ -ge 461808 } { "4.7.2"; break }
{ $_ -ge 461308 } { "4.7.1"; break }
{ $_ -ge 460798 } { "4.7"; break }
{ $_ -ge 394802 } { "4.6.2"; break }
{ $_ -ge 394254 } { "4.6.1"; break }
{ $_ -ge 393295 } { "4.6"; break }
{ $_ -ge 379893 } { "4.5.2"; break }
{ $_ -ge 378675 } { "4.5.1"; break }
{ $_ -ge 378389 } { "4.5"; break }
default { "Unknown" }
}
} else { $null }
$report.DotNetFramework = [ordered]@{
Net45PlusVersion = $net4Version
Net45PlusRelease = $net4Release
Components = $frameworks
}
#endregion
#region .NET Core / SDK
$dotnetPath = Get-Command dotnet -ErrorAction SilentlyContinue
if ($dotnetPath) {
$sdks = @(dotnet --list-sdks 2>$null | ForEach-Object {
if ($_ -match '^(\S+)\s+\[(.+)\]$') {
[ordered]@{
Version = $matches[1]
Location = $matches[2]
}
}
})
$runtimes = @(dotnet --list-runtimes 2>$null | ForEach-Object {
if ($_ -match '^(\S+)\s+(\S+)\s+\[(.+)\]$') {
[ordered]@{
Runtime = $matches[1]
Version = $matches[2]
Location = $matches[3]
}
}
})
$report.DotNetCore = [ordered]@{
CliLocation = $dotnetPath.Source
SDKs = $sdks
Runtimes = $runtimes
}
} else {
$report.DotNetCore = $null
}
#endregion
#region Installed Applications
$apps = @()
$regPaths = @(
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
)
foreach ($path in $regPaths) {
if (Test-Path $path) {
$apps += Get-ItemProperty $path -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and $_.DisplayName.Trim() } |
Select-Object DisplayName, DisplayVersion, Publisher, InstallDate
}
}
$report.InstalledApplications = [ordered]@{
TotalCount = $apps.Count
Items = @($apps | Sort-Object DisplayName -Unique | ForEach-Object {
[ordered]@{
Name = $_.DisplayName
Version = $_.DisplayVersion
Publisher = $_.Publisher
InstallDate = $_.InstallDate
}
})
}
#endregion
#region Development Tools
$tools = @(
@{Name="Git"; Cmd="git"; Args=@("--version")},
@{Name="Node.js"; Cmd="node"; Args=@("--version")},
@{Name="npm"; Cmd="npm"; Args=@("--version")},
@{Name="Python"; Cmd="python"; Args=@("--version")},
@{Name="Java"; Cmd="java"; Args=@("-version")},
@{Name="Maven"; Cmd="mvn"; Args=@("--version")},
@{Name="NuGet"; Cmd="nuget"; Args=@("help")},
@{Name="PowerShell"; Cmd="powershell"; Args=@("-Command", "`$PSVersionTable.PSVersion.ToString()")}
@{Name="PowerShell Core"; Cmd="pwsh"; Args=@("--version")},
@{Name="Docker"; Cmd="docker"; Args=@("--version")}
)
$devTools = @($tools | ForEach-Object {
$tool = $_
$cmd = Get-Command $tool.Cmd -ErrorAction SilentlyContinue
$result = [ordered]@{
Name = $tool.Name
Installed = $false
Version = $null
Path = $null
}
if ($cmd) {
try {
$output = & $cmd.Source $tool.Args 2>&1 | Out-String
if ($LASTEXITCODE -eq 0 -and $output -inotmatch "was not found|Microsoft Store") {
$ver = ($output.Split("`n") | Where-Object { $_.Trim() } | Select-Object -First 1).Trim()
$result.Installed = $true
$result.Version = $ver
$result.Path = $cmd.Source
}
} catch {}
}
$result
})
$report.DevelopmentTools = $devTools
#endregion
#region Visual Studio
$vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
if (Test-Path $vsWhere) {
$vsInstances = & $vsWhere -all -format json | ConvertFrom-Json
$report.VisualStudio = @($vsInstances | ForEach-Object {
[ordered]@{
DisplayName = $_.displayName
Version = $_.installationVersion
Channel = ($_.channelId -split '\.')[-1]
InstallationPath = $_.installationPath
ProductId = $_.productId
}
})
} else {
$report.VisualStudio = @()
}
#endregion
#region MSBuild
$msbuildInstalls = @()
if (Test-Path $vsWhere) {
$msbuildPaths = & $vsWhere -all -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe
foreach ($msbuildPath in $msbuildPaths) {
if (Test-Path $msbuildPath) {
$version = (Get-Item $msbuildPath).VersionInfo.ProductVersion
$vsProduct = if ($msbuildPath -match 'Visual Studio\\(\d{4})\\(\w+)\\') { "$($matches[1]) $($matches[2])" } else { $null }
$msbuildInstalls += [ordered]@{
Version = $version
Type = "Visual Studio"
VsProduct = $vsProduct
Path = $msbuildPath
}
}
}
}
$buildToolsPaths = @(
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2022\BuildTools\MSBuild\Current\Bin\MSBuild.exe",
"${env:ProgramFiles(x86)}\Microsoft Visual Studio\2019\BuildTools\MSBuild\Current\Bin\MSBuild.exe",
"${env:ProgramFiles(x86)}\MSBuild\14.0\Bin\MSBuild.exe",
"${env:ProgramFiles(x86)}\MSBuild\12.0\Bin\MSBuild.exe"
)
foreach ($path in $buildToolsPaths) {
if (Test-Path $path) {
$version = (Get-Item $path).VersionInfo.ProductVersion
$type = if ($path -match 'BuildTools') { "Build Tools" } else { "Standalone" }
$msbuildInstalls += [ordered]@{
Version = $version
Type = $type
VsProduct = $null
Path = $path
}
}
}
$msbuildInPath = Get-Command msbuild -ErrorAction SilentlyContinue
$report.MSBuild = [ordered]@{
InPath = if ($msbuildInPath) { $msbuildInPath.Source } else { $null }
Installations = $msbuildInstalls
}
#endregion
#region Current User Identity
$identity = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object System.Security.Principal.WindowsPrincipal($identity)
$groups = @($identity.Groups | ForEach-Object {
try {
$_.Translate([System.Security.Principal.NTAccount]).Value
} catch {
"$($_.Value) (unresolved)"
}
})
$report.CurrentUser = [ordered]@{
Username = $identity.Name
SID = $identity.User.Value
AuthenticationType = $identity.AuthenticationType
IsSystem = $identity.IsSystem
IsAdmin = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Groups = $groups | Where-Object { $_ -inotmatch '\(unresolved\)' }
}
#endregion
#region Key Windows Services
$keyServices = @(
'W3SVC', 'MSSQLSERVER', 'SQLSERVERAGENT', 'WinRM', 'Spooler',
'WSearch', 'wuauserv', 'EventLog', 'Schedule', 'LanmanServer', 'LanmanWorkstation'
)
$report.Services = @($keyServices | ForEach-Object {
$svcName = $_
$svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
if ($svc) {
$wmiSvc = Get-CimInstance Win32_Service -Filter "Name='$svcName'" -ErrorAction SilentlyContinue
[ordered]@{
Name = $svc.Name
DisplayName = $svc.DisplayName
Status = $svc.Status.ToString()
StartType = if ($wmiSvc) { $wmiSvc.StartMode } else { $null }
StartName = if ($wmiSvc) { $wmiSvc.StartName } else { $null }
}
}
} | Where-Object { $_ })
#endregion
return $report
}
# Execute locally or remotely
if ($ComputerName) {
$report = Invoke-Command @remoteParams -ScriptBlock $reportScript
} else {
$report = & $reportScript
}
# Convert to JSON
$jsonParams = @{ Depth = 100 }
# ConvertTo-Json doesn't have -Indent in PS 5.1, but it formats by default when Depth is set
if ($Compress) {
$jsonParams['Compress'] = $true
}
$json = $report | ConvertTo-Json @jsonParams
# Output to console
Write-Host $json
# Write to file if specified
if ($OutputPath) {
[System.IO.File]::WriteAllText($OutputPath, $json)
Write-Host ""
Write-Host "Report saved to: $OutputPath" -ForegroundColor Green
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment