microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
hve-core-v3.3.41

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/linting/Invoke-MsDateFreshnessCheck.ps1

309lines · 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 $markdown += "`n"
208
209 $sortedStaleFiles = $staleFiles | Sort-Object -Property AgeDays -Descending
210
211 foreach ($file in $sortedStaleFiles) {
212 $markdown += "| $($file.File) | $($file.MsDate) | $($file.AgeDays) |`n"
213 }
214 }
215 else {
216 $markdown += @"
217
218### ✅ All Files Fresh
219
220All documentation files with ms.date frontmatter are within the $Threshold-day freshness threshold.
221"@
222 }
223
224 $markdown | Out-File -FilePath $mdPath -Encoding utf8 -NoNewline
225
226 return @{
227 JsonPath = $jsonPath
228 MarkdownPath = $mdPath
229 StaleCount = @($staleFiles).Count
230 }
231}
232
233#endregion
234
235#region Main Logic
236
237Write-Verbose "Starting ms.date freshness check with $ThresholdDays-day threshold"
238
239$markdownFiles = @(Get-MarkdownFiles -SearchPaths $Paths -ChangedOnly:$ChangedFilesOnly -Base $BaseBranch)
240
241if (@($markdownFiles).Count -eq 0) {
242 Write-Warning "No markdown files found to check"
243 exit 0
244}
245
246Write-Verbose "Checking $(@($markdownFiles).Count) markdown files"
247
248$results = [System.Collections.Generic.List[PSCustomObject]]::new()
249$currentDate = Get-Date
250
251foreach ($file in $markdownFiles) {
252 $relativePath = if ($file -is [System.IO.FileInfo]) {
253 $file.FullName.Replace("$PWD$([System.IO.Path]::DirectorySeparatorChar)", '')
254 }
255 else {
256 $file.Replace("$PWD$([System.IO.Path]::DirectorySeparatorChar)", '')
257 }
258
259 $msDate = Get-MsDateFromFrontmatter -FilePath $file
260
261 if ($null -eq $msDate) {
262 Write-Verbose "Skipping $relativePath (no ms.date)"
263 continue
264 }
265
266 $age = $currentDate - $msDate
267 $ageDays = [int]$age.TotalDays
268 $isStale = $ageDays -gt $ThresholdDays
269
270 $result = [PSCustomObject]@{
271 File = $relativePath
272 MsDate = $msDate.ToString('yyyy-MM-dd')
273 AgeDays = $ageDays
274 IsStale = $isStale
275 Threshold = $ThresholdDays
276 }
277
278 $results.Add($result)
279
280 if ($isStale) {
281 Write-Verbose "Stale file detected: $relativePath ($ageDays days old)"
282 Write-CIAnnotation -Message "${relativePath}: ms.date is $ageDays days old (threshold: $ThresholdDays days)" -Level 'Warning' -File $relativePath
283 }
284}
285
286if (@($results).Count -eq 0) {
287 Write-Warning "No files with ms.date frontmatter found"
288 exit 0
289}
290
291$report = New-MsDateReport -Results $results -Threshold $ThresholdDays
292
293Write-Host "`nms.date Freshness Check Summary:"
294Write-Host " Files Checked: $(@($results).Count)"
295Write-Host " Stale Files: $($report.StaleCount)"
296Write-Host " Threshold: $ThresholdDays days"
297
298Write-CIStepSummary -Path $report.MarkdownPath
299
300if ($report.StaleCount -gt 0) {
301 Write-Host "`n❌ Found $($report.StaleCount) stale documentation file(s)"
302 exit 1
303}
304else {
305 Write-Host "`n✅ All files are fresh"
306 exit 0
307}
308
309#endregion
310