microsoft/hve-core

Public

mirrored fromhttps://github.com/microsoft/hve-coreAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
feat/add-pester-code-coverage

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

scripts/linting/Markdown-Link-Check.ps1

373lines · modecode

1#!/usr/bin/env pwsh
2#Requires -Version 7.0
3
4
5<#
6.SYNOPSIS
7 Repository-aware wrapper for markdown-link-check.
8
9.DESCRIPTION
10 Runs markdown-link-check with the repo-specific configuration to validate
11 all markdown links across the repository. Only checks files that are tracked
12 by git (respects .gitignore and only includes committed/staged files).
13
14.PARAMETER Path
15 One or more files or directories to scan. Directories are searched
16 recursively for Markdown files. Defaults to the Docsify navigation sources.
17
18.PARAMETER ConfigPath
19 Path to the shared markdown-link-check configuration file.
20
21.PARAMETER Quiet
22 Suppress non-error output from markdown-link-check.
23
24.EXAMPLE
25 # Validate all markdown files in default paths
26 ./Markdown-Link-Check.ps1
27
28.EXAMPLE
29 # Validate specific path with verbose output
30 ./Markdown-Link-Check.ps1 -Path ".github" -Quiet:$false
31 #>
32
33[CmdletBinding()]
34param(
35 [string[]]$Path = @(
36 ".",
37 ".github",
38 ".devcontainer"
39 ),
40
41 [string]$ConfigPath = (Join-Path -Path $PSScriptRoot -ChildPath 'markdown-link-check.config.json'),
42
43 [switch]$Quiet
44)
45
46# Import LintingHelpers module
47Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'Modules/LintingHelpers.psm1') -Force
48
49function Get-MarkdownTarget {
50 <#
51 .SYNOPSIS
52 Resolves Markdown files to validate from provided path arguments.
53
54 .DESCRIPTION
55 Accepts files or directories, expanding directories to all git-tracked
56 Markdown files discovered recursively, and returns a sorted, unique list
57 of absolute file paths for downstream validation. Only checks files that
58 are tracked by git (respects .gitignore).
59
60 .PARAMETER InputPath
61 Files or directories that may contain Markdown content.
62
63 .OUTPUTS
64 System.String[]
65 #>
66 param(
67 [string[]]$InputPath
68 )
69
70 $targets = @()
71 $repoRoot = git rev-parse --show-toplevel 2>$null
72
73 if ($LASTEXITCODE -ne 0) {
74 Write-Warning "Not in a git repository, falling back to file system search"
75 # Fallback to original implementation if not in git repo
76 foreach ($item in $InputPath) {
77 if ([string]::IsNullOrWhiteSpace($item)) {
78 continue
79 }
80
81 $resolved = Resolve-Path -LiteralPath $item -ErrorAction SilentlyContinue
82 if (-not $resolved) {
83 Write-Warning "Unable to resolve path: $item"
84 continue
85 }
86
87 foreach ($resolvedPath in $resolved) {
88 if (Test-Path -LiteralPath $resolvedPath -PathType Container) {
89 $targets += Get-ChildItem -LiteralPath $resolvedPath -Recurse -Include *.md |
90 Where-Object { -not $_.PSIsContainer } |
91 Select-Object -ExpandProperty FullName
92 }
93 else {
94 $targets += $resolvedPath.ProviderPath
95 }
96 }
97 }
98 return ($targets | Sort-Object -Unique)
99 }
100
101 Write-Verbose "Searching for git-tracked markdown files..."
102 Write-Verbose "Repository root: $repoRoot"
103
104 # Git-aware implementation
105 foreach ($item in $InputPath) {
106 if ([string]::IsNullOrWhiteSpace($item)) {
107 continue
108 }
109
110 # Check if it's a specific file or directory
111 if (Test-Path -Path $item -PathType Leaf) {
112 # Specific file - check if it's tracked by git
113 $absolutePath = (Resolve-Path $item).Path
114 $relativePath = [System.IO.Path]::GetRelativePath($repoRoot, $absolutePath)
115 $tracked = git ls-files $relativePath 2>$null
116
117 if ($tracked -and $item -like "*.md") {
118 $targets += $absolutePath
119 }
120 elseif (-not $tracked) {
121 Write-Warning "File not tracked by git: $item"
122 }
123 }
124 elseif (Test-Path -Path $item -PathType Container) {
125 # Directory - get all tracked markdown files
126 $absolutePath = (Resolve-Path $item).Path
127 $relativePath = [System.IO.Path]::GetRelativePath($repoRoot, $absolutePath)
128 $searchPath = if ($relativePath -eq '.') { '*.md' } else { "$relativePath/**/*.md" }
129
130 Write-Verbose "Searching in: $searchPath"
131 $trackedFiles = git ls-files $searchPath 2>$null
132
133 if ($trackedFiles) {
134 foreach ($file in $trackedFiles) {
135 $fullPath = Join-Path $repoRoot $file
136 if (Test-Path $fullPath) {
137 $targets += $fullPath
138 }
139 }
140 }
141 }
142 else {
143 Write-Warning "Unable to resolve path: $item"
144 }
145 }
146
147 Write-Verbose "Found $($targets.Count) git-tracked markdown files"
148 return ($targets | Sort-Object -Unique)
149}
150
151function Get-RelativePrefix {
152 <#
153 .SYNOPSIS
154 Builds a normalized relative prefix between two paths.
155
156 .DESCRIPTION
157 Computes the relative path from a source directory to a destination and
158 enforces forward-slash separators with a trailing slash when required to
159 produce consistent link prefixes.
160
161 .PARAMETER FromPath
162 The directory from which the relative path should be calculated.
163
164 .PARAMETER ToPath
165 The target path that should be expressed relative to the source.
166
167 .OUTPUTS
168 System.String
169 #>
170 param(
171 [string]$FromPath,
172 [string]$ToPath
173 )
174
175 $relative = [System.IO.Path]::GetRelativePath($FromPath, $ToPath)
176 if ([string]::IsNullOrWhiteSpace($relative) -or $relative -eq '.') {
177 return ''
178 }
179
180 $normalized = $relative -replace '\\', '/'
181 if (-not $normalized.EndsWith('/')) {
182 $normalized += '/'
183 }
184
185 return $normalized
186}
187
188
189$scriptRootParent = Split-Path -Path $PSScriptRoot -Parent
190$repoRootPath = Split-Path -Path $scriptRootParent -Parent
191$repoRoot = Resolve-Path -LiteralPath $repoRootPath
192$config = Resolve-Path -LiteralPath $ConfigPath -ErrorAction Stop
193$filesToCheck = Get-MarkdownTarget -InputPath $Path
194
195if (-not $filesToCheck -or $filesToCheck.Count -eq 0) {
196 Write-Error 'No markdown files were found to validate.'
197 exit 1
198}
199
200$cli = Join-Path -Path $repoRoot.Path -ChildPath 'node_modules/.bin/markdown-link-check'
201if ($IsWindows) {
202 $cli += '.cmd'
203}
204
205if (-not (Test-Path -LiteralPath $cli)) {
206 Write-Error 'markdown-link-check is not installed. Run "npm install --save-dev markdown-link-check" first.'
207 exit 1
208}
209
210$baseArguments = @('-c', $config.Path)
211if ($Quiet) {
212 $baseArguments += '-q'
213}
214
215$failedFiles = @()
216$brokenLinks = @()
217$totalLinks = 0
218$totalFiles = $filesToCheck.Count
219
220Push-Location $repoRoot.Path
221try {
222 foreach ($file in $filesToCheck) {
223 $absolute = Resolve-Path -LiteralPath $file
224 $relative = [System.IO.Path]::GetRelativePath($repoRoot.Path, $absolute)
225 Write-Output "Checking $relative"
226
227 # Create temp file for XML output
228 $xmlFile = [System.IO.Path]::GetTempFileName() + '.xml'
229 try {
230 $commandArgs = $baseArguments + @($relative, '--reporters', 'default,junit', '--junit-output', $xmlFile)
231
232 # Run markdown-link-check with XML output and capture output
233 $output = & $cli @commandArgs 2>&1
234 $exitCode = $LASTEXITCODE
235
236 # Display output if verbose mode or if there were errors
237 if ($VerbosePreference -eq 'Continue' -or $exitCode -ne 0) {
238 Write-Host $output
239 }
240
241 # Parse XML output
242 if (Test-Path $xmlFile) {
243 [xml]$xml = Get-Content $xmlFile -Raw -Encoding utf8
244
245 foreach ($testsuite in $xml.testsuites.testsuite) {
246 foreach ($testcase in $testsuite.testcase) {
247 $totalLinks++
248
249 # Extract properties
250 $url = ($testcase.properties.property | Where-Object { $_.name -eq 'url' }).value
251 $status = ($testcase.properties.property | Where-Object { $_.name -eq 'status' }).value
252 $statusCode = ($testcase.properties.property | Where-Object { $_.name -eq 'statusCode' }).value
253
254 # Display human-readable output if not quiet
255 if (-not $Quiet) {
256 if ($status -eq 'alive') {
257 Write-Host " ✓ $url" -ForegroundColor Green
258 }
259 elseif ($status -eq 'ignored') {
260 Write-Host " / $url (ignored)" -ForegroundColor Yellow
261 }
262 elseif ($status -eq 'dead') {
263 Write-Host " ✖ $url → Status: $statusCode" -ForegroundColor Red
264 }
265 }
266
267 # Process broken links
268 if ($status -eq 'dead') {
269 $brokenLinks += @{
270 File = $relative
271 Link = $url
272 Status = "$statusCode"
273 }
274
275 # Create GitHub annotation
276 Write-GitHubAnnotation -Type 'error' -Message "Broken link: $url (Status: $statusCode)" -File $relative
277 }
278 }
279 }
280 }
281
282 if ($exitCode -ne 0) {
283 $failedFiles += $relative
284 }
285 }
286 catch {
287 Write-Warning "Failed to parse XML output for $relative : $_"
288 if ($exitCode -ne 0) {
289 $failedFiles += $relative
290 }
291 }
292 finally {
293 if (Test-Path $xmlFile) {
294 Remove-Item $xmlFile -Force
295 }
296 }
297 }
298}
299finally {
300 Pop-Location
301}
302
303# Create logs directory and export results
304$logsDir = Join-Path -Path $repoRoot.Path -ChildPath 'logs'
305if (-not (Test-Path $logsDir)) {
306 New-Item -ItemType Directory -Path $logsDir -Force | Out-Null
307}
308
309$results = @{
310 timestamp = (Get-Date).ToUniversalTime().ToString('o')
311 script = 'markdown-link-check'
312 summary = @{
313 total_files = $totalFiles
314 files_with_broken_links = $failedFiles.Count
315 total_links_checked = $totalLinks
316 total_broken_links = $brokenLinks.Count
317 }
318 broken_links = $brokenLinks
319}
320
321$resultsPath = Join-Path -Path $logsDir -ChildPath 'markdown-link-check-results.json'
322$results | ConvertTo-Json -Depth 10 | Set-Content -Path $resultsPath -Encoding UTF8
323
324# Generate GitHub step summary
325if ($failedFiles.Count -gt 0) {
326 $summaryContent = @"
327## ❌ Markdown Link Check Failed
328
329**Files with broken links:** $($failedFiles.Count) / $totalFiles
330**Total broken links:** $($brokenLinks.Count)
331
332### Broken Links
333
334| File | Broken Link |
335|------|-------------|
336"@
337
338 foreach ($link in $brokenLinks) {
339 $summaryContent += "`n| ``$($link.File)`` | ``$($link.Link)`` |"
340 }
341
342 $summaryContent += @"
343
344
345### How to Fix
346
3471. Review the broken links listed above
3482. Update or remove invalid links
3493. Re-run the link check to verify fixes
350
351For more information, see the [markdown-link-check documentation](https://github.com/tcort/markdown-link-check).
352"@
353
354 Write-GitHubStepSummary -Content $summaryContent
355 Set-GitHubEnv -Name "MARKDOWN_LINK_CHECK_FAILED" -Value "true"
356
357 Write-Error ("markdown-link-check reported failures for: {0}" -f ($failedFiles -join ', '))
358 exit 1
359}
360else {
361 $summaryContent = @"
362## ✅ Markdown Link Check Passed
363
364**Files checked:** $totalFiles
365**Total links checked:** $totalLinks
366**Broken links:** 0
367
368Great job! All markdown links are valid. 🎉
369"@
370
371 Write-GitHubStepSummary -Content $summaryContent
372 Write-Output 'markdown-link-check completed successfully.'
373}
374