microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v2.0.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/linting/Markdown-Link-Check.ps1

384lines · 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#region Main Execution
189try {
190 $scriptRootParent = Split-Path -Path $PSScriptRoot -Parent
191 $repoRootPath = Split-Path -Path $scriptRootParent -Parent
192 $repoRoot = Resolve-Path -LiteralPath $repoRootPath
193 $config = Resolve-Path -LiteralPath $ConfigPath -ErrorAction Stop
194 $filesToCheck = Get-MarkdownTarget -InputPath $Path
195
196 if (-not $filesToCheck -or $filesToCheck.Count -eq 0) {
197 Write-Error 'No markdown files were found to validate.'
198 exit 1
199 }
200
201 $cli = Join-Path -Path $repoRoot.Path -ChildPath 'node_modules/.bin/markdown-link-check'
202 if ($IsWindows) {
203 $cli += '.cmd'
204 }
205
206 if (-not (Test-Path -LiteralPath $cli)) {
207 Write-Error 'markdown-link-check is not installed. Run "npm install --save-dev markdown-link-check" first.'
208 exit 1
209 }
210
211 $baseArguments = @('-c', $config.Path)
212 if ($Quiet) {
213 $baseArguments += '-q'
214 }
215
216 $failedFiles = @()
217 $brokenLinks = @()
218 $totalLinks = 0
219 $totalFiles = $filesToCheck.Count
220
221 Push-Location $repoRoot.Path
222 try {
223 foreach ($file in $filesToCheck) {
224 $absolute = Resolve-Path -LiteralPath $file
225 $relative = [System.IO.Path]::GetRelativePath($repoRoot.Path, $absolute)
226 Write-Output "Checking $relative"
227
228 # Create temp file for XML output
229 $xmlFile = [System.IO.Path]::GetTempFileName() + '.xml'
230 try {
231 $commandArgs = $baseArguments + @($relative, '--reporters', 'default,junit', '--junit-output', $xmlFile)
232
233 # Run markdown-link-check with XML output and capture output
234 $output = & $cli @commandArgs 2>&1
235 $exitCode = $LASTEXITCODE
236
237 # Display output if verbose mode or if there were errors
238 if ($VerbosePreference -eq 'Continue' -or $exitCode -ne 0) {
239 Write-Host $output
240 }
241
242 # Parse XML output
243 if (Test-Path $xmlFile) {
244 [xml]$xml = Get-Content $xmlFile -Raw -Encoding utf8
245
246 foreach ($testsuite in $xml.testsuites.testsuite) {
247 foreach ($testcase in $testsuite.testcase) {
248 $totalLinks++
249
250 # Extract properties
251 $url = ($testcase.properties.property | Where-Object { $_.name -eq 'url' }).value
252 $status = ($testcase.properties.property | Where-Object { $_.name -eq 'status' }).value
253 $statusCode = ($testcase.properties.property | Where-Object { $_.name -eq 'statusCode' }).value
254
255 # Display human-readable output if not quiet
256 if (-not $Quiet) {
257 if ($status -eq 'alive') {
258 Write-Host " ✓ $url" -ForegroundColor Green
259 }
260 elseif ($status -eq 'ignored') {
261 Write-Host " / $url (ignored)" -ForegroundColor Yellow
262 }
263 elseif ($status -eq 'dead') {
264 Write-Host " ✖ $url → Status: $statusCode" -ForegroundColor Red
265 }
266 }
267
268 # Process broken links
269 if ($status -eq 'dead') {
270 $brokenLinks += @{
271 File = $relative
272 Link = $url
273 Status = "$statusCode"
274 }
275
276 # Create GitHub annotation
277 Write-GitHubAnnotation -Type 'error' -Message "Broken link: $url (Status: $statusCode)" -File $relative
278 }
279 }
280 }
281 }
282
283 if ($exitCode -ne 0) {
284 $failedFiles += $relative
285 }
286 }
287 catch {
288 Write-Warning "Failed to parse XML output for $relative : $_"
289 if ($exitCode -ne 0) {
290 $failedFiles += $relative
291 }
292 }
293 finally {
294 if (Test-Path $xmlFile) {
295 Remove-Item $xmlFile -Force
296 }
297 }
298 }
299 }
300 finally {
301 Pop-Location
302 }
303
304 # Create logs directory and export results
305 $logsDir = Join-Path -Path $repoRoot.Path -ChildPath 'logs'
306 if (-not (Test-Path $logsDir)) {
307 New-Item -ItemType Directory -Path $logsDir -Force | Out-Null
308 }
309
310 $results = @{
311 timestamp = (Get-Date).ToUniversalTime().ToString('o')
312 script = 'markdown-link-check'
313 summary = @{
314 total_files = $totalFiles
315 files_with_broken_links = $failedFiles.Count
316 total_links_checked = $totalLinks
317 total_broken_links = $brokenLinks.Count
318 }
319 broken_links = $brokenLinks
320 }
321
322 $resultsPath = Join-Path -Path $logsDir -ChildPath 'markdown-link-check-results.json'
323 $results | ConvertTo-Json -Depth 10 | Set-Content -Path $resultsPath -Encoding UTF8
324
325 # Generate GitHub step summary
326 if ($failedFiles.Count -gt 0) {
327 $summaryContent = @"
328## ❌ Markdown Link Check Failed
329
330**Files with broken links:** $($failedFiles.Count) / $totalFiles
331**Total broken links:** $($brokenLinks.Count)
332
333### Broken Links
334
335| File | Broken Link |
336|------|-------------|
337"@
338
339 foreach ($link in $brokenLinks) {
340 $summaryContent += "`n| ``$($link.File)`` | ``$($link.Link)`` |"
341 }
342
343 $summaryContent += @"
344
345
346### How to Fix
347
3481. Review the broken links listed above
3492. Update or remove invalid links
3503. Re-run the link check to verify fixes
351
352For more information, see the [markdown-link-check documentation](https://github.com/tcort/markdown-link-check).
353"@
354
355 Write-GitHubStepSummary -Content $summaryContent
356 Set-GitHubEnv -Name "MARKDOWN_LINK_CHECK_FAILED" -Value "true"
357
358 Write-Error ("markdown-link-check reported failures for: {0}" -f ($failedFiles -join ', '))
359 exit 1
360 }
361 else {
362 $summaryContent = @"
363## ✅ Markdown Link Check Passed
364
365**Files checked:** $totalFiles
366**Total links checked:** $totalLinks
367**Broken links:** 0
368
369Great job! All markdown links are valid. 🎉
370"@
371
372 Write-GitHubStepSummary -Content $summaryContent
373 Write-Output 'markdown-link-check completed successfully.'
374 exit 0
375 }
376}
377catch {
378 Write-Error "Markdown Link Check failed: $($_.Exception.Message)"
379 if ($env:GITHUB_ACTIONS -eq 'true') {
380 Write-Output "::error::$($_.Exception.Message)"
381 }
382 exit 1
383}
384#endregion