microsoft/hve-core

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
docs/227-add-governance

Branches

Tags

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

Clone

HTTPS

Download ZIP

scripts/linting/Modules/LintingHelpers.psm1

326lines · modecode

1# LintingHelpers.psm1
2#
3# Purpose: Shared helper functions for linting scripts and workflows
4# Author: HVE Core Team
5# Created: 2025-11-05
6
7function Get-ChangedFilesFromGit {
8 <#
9 .SYNOPSIS
10 Gets changed files from git with intelligent fallback strategies.
11
12 .DESCRIPTION
13 Attempts to detect changed files using merge-base, with fallbacks for different scenarios.
14
15 .PARAMETER BaseBranch
16 The base branch to compare against (default: origin/main).
17
18 .PARAMETER FileExtensions
19 Array of file extensions to filter (e.g., @('*.ps1', '*.md')).
20
21 .OUTPUTS
22 Array of changed file paths.
23 #>
24 [CmdletBinding()]
25 param(
26 [Parameter(Mandatory = $false)]
27 [string]$BaseBranch = "origin/main",
28
29 [Parameter(Mandatory = $false)]
30 [string[]]$FileExtensions = @('*')
31 )
32
33 $changedFiles = @()
34
35 try {
36 # Try merge-base first (best for PRs)
37 $mergeBase = git merge-base HEAD $BaseBranch 2>$null
38
39 if ($LASTEXITCODE -eq 0 -and $mergeBase) {
40 Write-Verbose "Using merge-base: $mergeBase"
41 $changedFiles = git diff --name-only --diff-filter=ACMR $mergeBase HEAD 2>$null
42 }
43 elseif ((git rev-parse HEAD~1 2>$null)) {
44 Write-Verbose "Merge base failed, using HEAD~1"
45 $changedFiles = git diff --name-only --diff-filter=ACMR HEAD~1 HEAD 2>$null
46 }
47 else {
48 Write-Verbose "HEAD~1 failed, using staged/unstaged files"
49 $changedFiles = git diff --name-only HEAD 2>$null
50 }
51
52 if ($LASTEXITCODE -ne 0) {
53 Write-Warning "Unable to determine changed files from git"
54 return @()
55 }
56
57 # Filter by extensions and verify files exist
58 $filteredFiles = $changedFiles | Where-Object {
59 if ([string]::IsNullOrEmpty($_)) { return $false }
60
61 # Check if file matches any of the allowed extensions
62 $currentFile = $_
63 $matchesExtension = $false
64 foreach ($pattern in $FileExtensions) {
65 if ($currentFile -like $pattern) {
66 $matchesExtension = $true
67 break
68 }
69 }
70
71 $matchesExtension -and (Test-Path $currentFile -PathType Leaf)
72 }
73
74 Write-Verbose "Found $($filteredFiles.Count) changed files matching extensions: $($FileExtensions -join ', ')"
75 return $filteredFiles
76 }
77 catch {
78 Write-Warning "Error getting changed files: $($_.Exception.Message)"
79 return @()
80 }
81}
82
83function Get-FilesRecursive {
84 <#
85 .SYNOPSIS
86 Gets files recursively with gitignore filtering.
87
88 .DESCRIPTION
89 Recursively finds files by extension, respecting .gitignore patterns.
90
91 .PARAMETER Path
92 Root path to search from.
93
94 .PARAMETER Include
95 File patterns to include (e.g., @('*.ps1', '*.psm1')).
96
97 .PARAMETER GitIgnorePath
98 Path to .gitignore file for exclusion patterns.
99
100 .OUTPUTS
101 Array of FileInfo objects.
102 #>
103 [CmdletBinding()]
104 param(
105 [Parameter(Mandatory = $true)]
106 [string]$Path,
107
108 [Parameter(Mandatory = $true)]
109 [string[]]$Include,
110
111 [Parameter(Mandatory = $false)]
112 [string]$GitIgnorePath
113 )
114
115 $files = Get-ChildItem -Path $Path -Recurse -Include $Include -File -ErrorAction SilentlyContinue
116
117 # Apply gitignore filtering if provided
118 if ($GitIgnorePath -and (Test-Path $GitIgnorePath)) {
119 $gitignorePatterns = Get-GitIgnorePatterns -GitIgnorePath $GitIgnorePath
120
121 $files = $files | Where-Object {
122 $file = $_
123 $excluded = $false
124
125 foreach ($pattern in $gitignorePatterns) {
126 if ($file.FullName -like $pattern) {
127 $excluded = $true
128 break
129 }
130 }
131
132 -not $excluded
133 }
134 }
135
136 return $files
137}
138
139function Get-GitIgnorePatterns {
140 <#
141 .SYNOPSIS
142 Parses .gitignore into PowerShell wildcard patterns.
143
144 .PARAMETER GitIgnorePath
145 Path to .gitignore file.
146
147 .OUTPUTS
148 Array of wildcard patterns using platform-appropriate separators.
149 #>
150 [CmdletBinding()]
151 param(
152 [Parameter(Mandatory = $true)]
153 [string]$GitIgnorePath
154 )
155
156 if (-not (Test-Path $GitIgnorePath)) {
157 return @()
158 }
159
160 $sep = [System.IO.Path]::DirectorySeparatorChar
161
162 $patterns = Get-Content $GitIgnorePath | Where-Object {
163 $_ -and -not $_.StartsWith('#') -and $_.Trim() -ne ''
164 } | ForEach-Object {
165 $pattern = $_.Trim()
166
167 # Normalize to platform separator
168 $normalizedPattern = $pattern.Replace('/', $sep).Replace('\', $sep)
169
170 if ($pattern.EndsWith('/')) {
171 "*$sep$($normalizedPattern.TrimEnd($sep))$sep*"
172 }
173 elseif ($pattern.Contains('/') -or $pattern.Contains('\')) {
174 "*$sep$normalizedPattern*"
175 }
176 else {
177 "*$sep$normalizedPattern$sep*"
178 }
179 }
180
181 return $patterns
182}
183
184function Write-GitHubAnnotation {
185 <#
186 .SYNOPSIS
187 Writes GitHub Actions annotations for errors, warnings, or notices.
188
189 .PARAMETER Type
190 Annotation type: 'error', 'warning', or 'notice'.
191
192 .PARAMETER Message
193 The annotation message.
194
195 .PARAMETER File
196 Optional file path.
197
198 .PARAMETER Line
199 Optional line number.
200
201 .PARAMETER Column
202 Optional column number.
203 #>
204 [CmdletBinding()]
205 param(
206 [Parameter(Mandatory = $true)]
207 [ValidateSet('error', 'warning', 'notice')]
208 [string]$Type,
209
210 [Parameter(Mandatory = $true)]
211 [string]$Message,
212
213 [Parameter(Mandatory = $false)]
214 [string]$File,
215
216 [Parameter(Mandatory = $false)]
217 [int]$Line,
218
219 [Parameter(Mandatory = $false)]
220 [int]$Column
221 )
222
223 $annotation = "::${Type}"
224
225 $properties = @()
226 if ($File) { $properties += "file=$File" }
227 if ($Line -gt 0) { $properties += "line=$Line" }
228 if ($Column -gt 0) { $properties += "col=$Column" }
229
230 if ($properties.Count -gt 0) {
231 $annotation += " $($properties -join ',')"
232 }
233
234 $annotation += "::$Message"
235
236 Write-Host $annotation
237}
238
239function Set-GitHubOutput {
240 <#
241 .SYNOPSIS
242 Sets GitHub Actions output variable.
243
244 .PARAMETER Name
245 Output variable name.
246
247 .PARAMETER Value
248 Output value.
249 #>
250 [CmdletBinding()]
251 param(
252 [Parameter(Mandatory = $true)]
253 [string]$Name,
254
255 [Parameter(Mandatory = $true)]
256 [string]$Value
257 )
258
259 if ($env:GITHUB_OUTPUT) {
260 "$Name=$Value" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
261 }
262 else {
263 Write-Verbose "Not in GitHub Actions environment - output: $Name=$Value"
264 }
265}
266
267function Set-GitHubEnv {
268 <#
269 .SYNOPSIS
270 Sets GitHub Actions environment variable.
271
272 .PARAMETER Name
273 Environment variable name.
274
275 .PARAMETER Value
276 Environment value.
277 #>
278 [CmdletBinding()]
279 param(
280 [Parameter(Mandatory = $true)]
281 [string]$Name,
282
283 [Parameter(Mandatory = $true)]
284 [string]$Value
285 )
286
287 if ($env:GITHUB_ENV) {
288 "$Name=$Value" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
289 }
290 else {
291 Write-Verbose "Not in GitHub Actions environment - env: $Name=$Value"
292 }
293}
294
295function Write-GitHubStepSummary {
296 <#
297 .SYNOPSIS
298 Appends content to GitHub Actions step summary.
299
300 .PARAMETER Content
301 Markdown content to append.
302 #>
303 [CmdletBinding()]
304 param(
305 [Parameter(Mandatory = $true)]
306 [string]$Content
307 )
308
309 if ($env:GITHUB_STEP_SUMMARY) {
310 $Content | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
311 }
312 else {
313 Write-Verbose "Not in GitHub Actions environment - summary content: $Content"
314 }
315}
316
317# Export functions
318Export-ModuleMember -Function @(
319 'Get-ChangedFilesFromGit',
320 'Get-FilesRecursive',
321 'Get-GitIgnorePatterns',
322 'Write-GitHubAnnotation',
323 'Set-GitHubOutput',
324 'Set-GitHubEnv',
325 'Write-GitHubStepSummary'
326)