microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
3a3a0fdf923d96a9e8a9ac734c73f24433b525e8

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/linting/Invoke-MsDateFreshnessCheck.ps1

308lines · modecode

1#!/usr/bin/env pwsh
2# Copyright (c) Microsoft Corporation.
3# SPDX-License-Identifier: MIT
4<#
5.SYNOPSIS
6 Checks ms.date frontmatter freshness in markdown files.
7
8.DESCRIPTION
9 Scans markdown files for ms.date frontmatter and flags files where the date
10 exceeds a configurable staleness threshold. Generates JSON report and markdown
11 summary for GitHub Actions job summaries.
12
13.PARAMETER ThresholdDays
14 Number of days before ms.date is considered stale. Defaults to 90.
15
16.PARAMETER Paths
17 Directories to scan for markdown files. Defaults to repository root.
18
19.PARAMETER ChangedFilesOnly
20 Only check files changed relative to BaseBranch.
21
22.PARAMETER BaseBranch
23 Base branch for changed-file detection. Defaults to 'origin/main'.
24#>
25
26#Requires -Version 7.0
27
28[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Parameters consumed via script scope')]
29[CmdletBinding()]
30param(
31 [Parameter()]
32 [int]$ThresholdDays = 90,
33
34 [Parameter()]
35 [string[]]$Paths = @('.'),
36
37 [Parameter()]
38 [switch]$ChangedFilesOnly,
39
40 [Parameter()]
41 [string]$BaseBranch = 'origin/main'
42)
43
44$ErrorActionPreference = 'Stop'
45Set-StrictMode -Version Latest
46
47$scriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { Split-Path -Parent $MyInvocation.MyCommand.Definition }
48Import-Module (Join-Path $scriptRoot 'Modules' 'LintingHelpers.psm1') -Force
49Import-Module (Join-Path $scriptRoot '..' 'lib' 'Modules' 'CIHelpers.psm1') -Force
50
51#region Helper Functions
52
53function Get-MarkdownFiles {
54 [CmdletBinding()]
55 param(
56 [Parameter(Mandatory = $true)]
57 [string[]]$SearchPaths,
58
59 [Parameter(Mandatory = $false)]
60 [switch]$ChangedOnly,
61
62 [Parameter(Mandatory = $false)]
63 [string]$Base = 'origin/main'
64 )
65
66 if ($ChangedOnly) {
67 Write-Verbose "Getting changed markdown files relative to $Base"
68 $files = @(Get-ChangedFilesFromGit -BaseBranch $Base -FileExtensions @('*.md'))
69 return @($files | Where-Object { Test-Path $_ -PathType Leaf })
70 }
71
72 $excludePatterns = @('node_modules', '.git', 'logs', '.copilot-tracking', 'CHANGELOG.md')
73 $allFiles = @()
74
75 # Bypass exclusions only when the caller passes a single explicit file path.
76 # Directory paths (including '.' or absolute paths) always receive standard exclusions.
77 $isExplicitFilePath = @($SearchPaths).Count -eq 1 -and (Test-Path $SearchPaths[0] -PathType Leaf)
78
79 foreach ($path in $SearchPaths) {
80 if (-not (Test-Path $path)) {
81 Write-Warning "Path not found: $path"
82 continue
83 }
84
85 $files = @(Get-ChildItem -Path $path -Recurse -Include '*.md' -File -ErrorAction SilentlyContinue)
86
87 $allFiles += @($files | Where-Object {
88 $file = $_
89
90 if ($isExplicitFilePath) {
91 return $true
92 }
93
94 $excluded = $false
95 foreach ($pattern in $excludePatterns) {
96 if ($file.FullName -like "*$([System.IO.Path]::DirectorySeparatorChar)$pattern$([System.IO.Path]::DirectorySeparatorChar)*" -or
97 $file.FullName -like "*$([System.IO.Path]::DirectorySeparatorChar)$pattern" -or
98 $file.Name -eq $pattern) {
99 $excluded = $true
100 break
101 }
102 }
103
104 -not $excluded
105 })
106 }
107
108 Write-Verbose "Found $(@($allFiles).Count) markdown files"
109 return $allFiles
110}
111
112function Get-MsDateFromFrontmatter {
113 [CmdletBinding()]
114 param(
115 [Parameter(Mandatory = $true)]
116 [string]$FilePath
117 )
118
119 try {
120 $content = Get-Content -Path $FilePath -Raw -ErrorAction Stop
121
122 if ($content -match '(?s)^---\r?\n(.*?)\r?\n---') {
123 $yamlContent = $matches[1]
124
125 if (-not (Get-Command ConvertFrom-Yaml -ErrorAction SilentlyContinue)) {
126 Write-Warning "PowerShell-Yaml module not found. Install with: Install-Module PowerShell-Yaml"
127 return $null
128 }
129
130 try {
131 $frontmatter = $yamlContent | ConvertFrom-Yaml
132
133 if ($frontmatter -and $frontmatter.'ms.date') {
134 $msDateString = $frontmatter.'ms.date'
135
136 try {
137 $msDate = [DateTime]::ParseExact(
138 $msDateString,
139 'yyyy-MM-dd',
140 [Globalization.CultureInfo]::InvariantCulture
141 )
142 return $msDate
143 }
144 catch {
145 Write-Verbose "Invalid ms.date format in ${FilePath}: $msDateString"
146 return $null
147 }
148 }
149 }
150 catch {
151 Write-Verbose "Failed to parse YAML frontmatter in ${FilePath}: $($_.Exception.Message)"
152 return $null
153 }
154 }
155
156 return $null
157 }
158 catch {
159 Write-Warning "Error reading file ${FilePath}: $($_.Exception.Message)"
160 return $null
161 }
162}
163
164function New-MsDateReport {
165 [CmdletBinding()]
166 param(
167 [Parameter(Mandatory = $true)]
168 [array]$Results,
169
170 [Parameter(Mandatory = $true)]
171 [int]$Threshold,
172
173 [Parameter()]
174 [string]$OutputDirectory = ''
175 )
176
177 $logsDir = if ($OutputDirectory) { $OutputDirectory } else { Join-Path $PSScriptRoot '..' '..' 'logs' }
178 if (-not (Test-Path $logsDir)) {
179 New-Item -ItemType Directory -Path $logsDir -Force | Out-Null
180 }
181
182 $jsonPath = Join-Path $logsDir 'msdate-freshness-results.json'
183 $mdPath = Join-Path $logsDir 'msdate-summary.md'
184
185 $Results | ConvertTo-Json -Depth 10 | Out-File -FilePath $jsonPath -Encoding utf8
186 Write-Verbose "JSON report written to $jsonPath"
187
188 $staleFiles = @($Results | Where-Object { $_.IsStale })
189 $totalFiles = @($Results).Count
190
191 $markdown = @"
192# ms.date Freshness Check Results
193
194**Threshold**: $Threshold days
195**Files Checked**: $totalFiles
196**Stale Files**: $(@($staleFiles).Count)
197"@
198
199 if (@($staleFiles).Count -gt 0) {
200 $markdown += @"
201
202## 🚨 Stale Documentation Files
203
204| File | ms.date | Age (days) |
205|------|---------|------------|
206"@
207
208 $sortedStaleFiles = $staleFiles | Sort-Object -Property AgeDays -Descending
209
210 foreach ($file in $sortedStaleFiles) {
211 $markdown += "| $($file.File) | $($file.MsDate) | $($file.AgeDays) |`n"
212 }
213 }
214 else {
215 $markdown += @"
216
217### ✅ All Files Fresh
218
219All documentation files with ms.date frontmatter are within the $Threshold-day freshness threshold.
220"@
221 }
222
223 $markdown | Out-File -FilePath $mdPath -Encoding utf8 -NoNewline
224
225 return @{
226 JsonPath = $jsonPath
227 MarkdownPath = $mdPath
228 StaleCount = @($staleFiles).Count
229 }
230}
231
232#endregion
233
234#region Main Logic
235
236Write-Verbose "Starting ms.date freshness check with $ThresholdDays-day threshold"
237
238$markdownFiles = @(Get-MarkdownFiles -SearchPaths $Paths -ChangedOnly:$ChangedFilesOnly -Base $BaseBranch)
239
240if (@($markdownFiles).Count -eq 0) {
241 Write-Warning "No markdown files found to check"
242 exit 0
243}
244
245Write-Verbose "Checking $(@($markdownFiles).Count) markdown files"
246
247$results = [System.Collections.Generic.List[PSCustomObject]]::new()
248$currentDate = Get-Date
249
250foreach ($file in $markdownFiles) {
251 $relativePath = if ($file -is [System.IO.FileInfo]) {
252 $file.FullName.Replace("$PWD$([System.IO.Path]::DirectorySeparatorChar)", '')
253 }
254 else {
255 $file.Replace("$PWD$([System.IO.Path]::DirectorySeparatorChar)", '')
256 }
257
258 $msDate = Get-MsDateFromFrontmatter -FilePath $file
259
260 if ($null -eq $msDate) {
261 Write-Verbose "Skipping $relativePath (no ms.date)"
262 continue
263 }
264
265 $age = $currentDate - $msDate
266 $ageDays = [int]$age.TotalDays
267 $isStale = $ageDays -gt $ThresholdDays
268
269 $result = [PSCustomObject]@{
270 File = $relativePath
271 MsDate = $msDate.ToString('yyyy-MM-dd')
272 AgeDays = $ageDays
273 IsStale = $isStale
274 Threshold = $ThresholdDays
275 }
276
277 $results.Add($result)
278
279 if ($isStale) {
280 Write-Verbose "Stale file detected: $relativePath ($ageDays days old)"
281 Write-CIAnnotation -Message "${relativePath}: ms.date is $ageDays days old (threshold: $ThresholdDays days)" -Level 'Warning' -File $relativePath
282 }
283}
284
285if (@($results).Count -eq 0) {
286 Write-Warning "No files with ms.date frontmatter found"
287 exit 0
288}
289
290$report = New-MsDateReport -Results $results -Threshold $ThresholdDays
291
292Write-Host "`nms.date Freshness Check Summary:"
293Write-Host " Files Checked: $(@($results).Count)"
294Write-Host " Stale Files: $($report.StaleCount)"
295Write-Host " Threshold: $ThresholdDays days"
296
297Write-CIStepSummary -Path $report.MarkdownPath
298
299if ($report.StaleCount -gt 0) {
300 Write-Host "`n❌ Found $($report.StaleCount) stale documentation file(s)"
301 exit 1
302}
303else {
304 Write-Host "`n✅ All files are fresh"
305 exit 0
306}
307
308#endregion
309