MegaBites-AI/Windows-powershell
0372
1# Copyright (c) Microsoft Corporation.2# Licensed under the MIT License.3 4#Requires -Version 7.05 6<#7.SYNOPSIS8 Verify all links in markdown files.9 10.DESCRIPTION11 This script parses markdown files to extract links and verifies their accessibility.12 It supports HTTP/HTTPS links and local file references.13 14.PARAMETER Path15 Path to the directory containing markdown files. Defaults to current directory.16 17.PARAMETER File18 Array of specific markdown files to verify. If provided, Path parameter is ignored.19 20.PARAMETER TimeoutSec21 Timeout in seconds for HTTP requests. Defaults to 30.22 23.PARAMETER MaximumRetryCount24 Maximum number of retries for failed requests. Defaults to 2.25 26.PARAMETER RetryIntervalSec27 Interval in seconds between retry attempts. Defaults to 2.28 29.EXAMPLE30 .\Verify-MarkdownLinks.ps1 -Path ./CHANGELOG31 32.EXAMPLE33 .\Verify-MarkdownLinks.ps1 -Path ./docs -FailOnError34 35.EXAMPLE36 .\Verify-MarkdownLinks.ps1 -File @('CHANGELOG/7.5.md', 'README.md')37#>38 39param(40 [Parameter(ParameterSetName = 'ByPath', Mandatory)]41 [string]$Path = "Q:\src\git\powershell\docs\git",42 [Parameter(ParameterSetName = 'ByFile', Mandatory)]43 [string[]]$File = @(),44 [int]$TimeoutSec = 30,45 [int]$MaximumRetryCount = 2,46 [int]$RetryIntervalSec = 247)48 49$ErrorActionPreference = 'Stop'50 51# Get the script directory52$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path53 54# Determine what to process: specific files or directory55if ($File.Count -gt 0) {56 Write-Host "Extracting links from $($File.Count) specified markdown file(s)" -ForegroundColor Cyan57 58 # Process each file individually59 $allLinks = @()60 $parseScriptPath = Join-Path $scriptDir "Parse-MarkdownLink.ps1"61 62 foreach ($filePath in $File) {63 if (Test-Path $filePath) {64 Write-Verbose "Processing: $filePath"65 $fileLinks = & $parseScriptPath -ChangelogPath $filePath66 $allLinks += $fileLinks67 }68 else {69 Write-Warning "File not found: $filePath"70 }71 }72}73else {74 Write-Host "Extracting links from markdown files in: $Path" -ForegroundColor Cyan75 76 # Get all links from markdown files using the Parse-ChangelogLinks script77 $parseScriptPath = Join-Path $scriptDir "Parse-MarkdownLink.ps1"78 $allLinks = & $parseScriptPath -ChangelogPath $Path79}80 81if ($allLinks.Count -eq 0) {82 Write-Host "No links found in markdown files." -ForegroundColor Yellow83 exit 084}85 86Write-Host "Found $($allLinks.Count) links to verify" -ForegroundColor Green87 88# Group links by URL to avoid duplicate checks89$uniqueLinks = $allLinks | Group-Object -Property Url90 91Write-Host "Unique URLs to verify: $($uniqueLinks.Count)" -ForegroundColor Cyan92 93$results = @{94 Total = $uniqueLinks.Count95 Passed = 096 Failed = 097 Skipped = 098 Errors = [System.Collections.ArrayList]::new()99}100 101function Test-HttpLink {102 param(103 [string]$Url104 )105 106 try {107 # Try HEAD request first (faster, doesn't download content)108 $response = Invoke-WebRequest -Uri $Url `109 -Method Head `110 -TimeoutSec $TimeoutSec `111 -MaximumRetryCount $MaximumRetryCount `112 -RetryIntervalSec $RetryIntervalSec `113 -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com/PowerShell/PowerShell)" `114 -SkipHttpErrorCheck115 116 # If HEAD fails with 404 or 405, retry with GET (some servers don't support HEAD)117 if ($response.StatusCode -eq 404 -or $response.StatusCode -eq 405) {118 Write-Verbose "HEAD request failed with $($response.StatusCode), retrying with GET for: $Url"119 $response = Invoke-WebRequest -Uri $Url `120 -Method Get `121 -TimeoutSec $TimeoutSec `122 -MaximumRetryCount $MaximumRetryCount `123 -RetryIntervalSec $RetryIntervalSec `124 -UserAgent "Mozilla/5.0 (compatible; GitHubActions/1.0; +https://github.com)" `125 -SkipHttpErrorCheck126 }127 128 if ($response.StatusCode -ge 200 -and $response.StatusCode -lt 400) {129 return @{ Success = $true; StatusCode = $response.StatusCode }130 }131 else {132 return @{ Success = $false; StatusCode = $response.StatusCode; Error = "HTTP $($response.StatusCode)" }133 }134 }135 catch {136 return @{ Success = $false; StatusCode = 0; Error = $_.Exception.Message }137 }138}139 140function Test-LocalLink {141 param(142 [string]$Url,143 [string]$BasePath144 )145 146 # Strip query parameters (e.g., ?sanitize=true) and anchors (e.g., #section)147 $cleanUrl = $Url -replace '\?.*$', '' -replace '#.*$', ''148 149 # Handle relative paths150 $targetPath = Join-Path $BasePath $cleanUrl151 152 if (Test-Path $targetPath) {153 return @{ Success = $true }154 }155 else {156 return @{ Success = $false; Error = "File not found: $targetPath" }157 }158}159 160# Verify each unique link161$progressCount = 0162foreach ($linkGroup in $uniqueLinks) {163 $progressCount++164 $url = $linkGroup.Name165 $occurrences = $linkGroup.Group166 Write-Verbose -Verbose "[$progressCount/$($uniqueLinks.Count)] Checking: $url"167 168 # Determine link type and verify169 $verifyResult = $null170 if ($url -match '^https?://') {171 $verifyResult = Test-HttpLink -Url $url172 }173 elseif ($url -match '^#') {174 Write-Verbose -Verbose "Skipping anchor link: $url"175 $results.Skipped++176 continue177 }178 elseif ($url -match '^mailto:') {179 Write-Verbose -Verbose "Skipping mailto link: $url"180 $results.Skipped++181 continue182 }183 else {184 $basePath = Split-Path -Parent $occurrences[0].Path185 $verifyResult = Test-LocalLink -Url $url -BasePath $basePath186 }187 if ($verifyResult.Success) {188 Write-Host "✓ OK: $url" -ForegroundColor Green189 $results.Passed++190 }191 else {192 $errorMsg = if ($verifyResult.StatusCode) {193 "HTTP $($verifyResult.StatusCode)"194 }195 else {196 $verifyResult.Error197 }198 199 # Determine if this status code should be ignored or treated as failure200 # Ignore: 401 (Unauthorized), 403 (Forbidden), 429 (Too Many Requests - already retried)201 # Fail: 404 (Not Found), 410 (Gone), 406 (Not Acceptable) - these indicate broken links202 $shouldIgnore = $false203 $ignoreReason = ""204 205 switch ($verifyResult.StatusCode) {206 401 {207 $shouldIgnore = $true208 $ignoreReason = "authentication required"209 }210 403 {211 $shouldIgnore = $true212 $ignoreReason = "access forbidden"213 }214 429 {215 $shouldIgnore = $true216 $ignoreReason = "rate limited (already retried)"217 }218 }219 220 if ($shouldIgnore) {221 Write-Host "⊘ IGNORED: $url - $errorMsg ($ignoreReason)" -ForegroundColor Yellow222 Write-Verbose -Verbose "Ignored error details for $url - Status: $($verifyResult.StatusCode) - $ignoreReason"223 foreach ($occurrence in $occurrences) {224 Write-Verbose -Verbose " Found in: $($occurrence.Path):$($occurrence.Line):$($occurrence.Column)"225 }226 $results.Skipped++227 }228 else {229 Write-Host "✗ FAILED: $url - $errorMsg" -ForegroundColor Red230 foreach ($occurrence in $occurrences) {231 Write-Host " Found in: $($occurrence.Path):$($occurrence.Line):$($occurrence.Column)" -ForegroundColor DarkGray232 }233 $results.Failed++234 [void]$results.Errors.Add(@{235 Url = $url236 Error = $errorMsg237 Occurrences = $occurrences238 })239 }240 }241 }242 243# Print summary244Write-Host "`n" + ("=" * 60) -ForegroundColor Cyan245Write-Host "Link Verification Summary" -ForegroundColor Cyan246Write-Host ("=" * 60) -ForegroundColor Cyan247Write-Host "Total URLs checked: $($results.Total)" -ForegroundColor White248Write-Host "Passed: $($results.Passed)" -ForegroundColor Green249Write-Host "Failed: $($results.Failed)" -ForegroundColor $(if ($results.Failed -gt 0) { "Red" } else { "Green" })250Write-Host "Skipped: $($results.Skipped)" -ForegroundColor Gray251 252if ($results.Failed -gt 0) {253 Write-Host "`nFailed Links:" -ForegroundColor Red254 foreach ($failedLink in $results.Errors) {255 Write-Host " • $($failedLink.Url)" -ForegroundColor Red256 Write-Host " Error: $($failedLink.Error)" -ForegroundColor DarkGray257 Write-Host " Occurrences: $($failedLink.Occurrences.Count)" -ForegroundColor DarkGray258 }259 260 Write-Host "`n❌ Link verification failed!" -ForegroundColor Red261 exit 1262}263else {264 Write-Host "`n✅ All links verified successfully!" -ForegroundColor Green265}266 267# Write to GitHub Actions step summary if running in a workflow268if ($env:GITHUB_STEP_SUMMARY) {269 $summaryContent = @"270 271# Markdown Link Verification Results272 273## Summary274- **Total URLs checked:** $($results.Total)275- **Passed:** ✅ $($results.Passed)276- **Failed:** $(if ($results.Failed -gt 0) { "❌" } else { "✅" }) $($results.Failed)277- **Skipped:** $($results.Skipped)278 279"@280 281 if ($results.Failed -gt 0) {282 $summaryContent += @"283 284## Failed Links285 286| URL | Error | Occurrences |287|-----|-------|-------------|288 289"@290 foreach ($failedLink in $results.Errors) {291 $summaryContent += "| $($failedLink.Url) | $($failedLink.Error) | $($failedLink.Occurrences.Count) |`n"292 }293 294 $summaryContent += @"295 296<details>297<summary>Click to see all failed link locations</summary>298 299"@300 foreach ($failedLink in $results.Errors) {301 $summaryContent += "`n### $($failedLink.Url)`n"302 $summaryContent += "**Error:** $($failedLink.Error)`n`n"303 foreach ($occurrence in $failedLink.Occurrences) {304 $summaryContent += "- `$($occurrence.Path):$($occurrence.Line):$($occurrence.Column)`n"305 }306 }307 $summaryContent += "`n</details>`n"308 }309 else {310 $summaryContent += "`n## ✅ All links verified successfully!`n"311 }312 313 Write-Verbose -Verbose "Writing `n $summaryContent `n to ${env:GITHUB_STEP_SUMMARY}"314 $summaryContent | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append315 Write-Verbose -Verbose "Summary written to GitHub Actions step summary"316}317 318 