Skip to content

Commit 64db98d

Browse files
fix(scripts): add FooterExcludePaths parameter to frontmatter validation (#334)
# Pull Request ## Description Add `FooterExcludePaths` parameter to frontmatter validation, enabling per-file exclusion from footer validation without skipping footer checks entirely. **Changes:** - Add `FooterExcludePaths` parameter to `Test-SingleFileFrontmatter` with wildcard pattern support - Thread parameter through `Invoke-FrontmatterValidation` and `Validate-MarkdownFrontmatter.ps1` - Add `footer-exclude-paths` workflow input with `CHANGELOG.md` default (comma-separated list support) - Normalize path separators for cross-platform pattern matching (forward slashes) - Add 6 unit tests covering exclusion logic, non-excluded files, wildcard patterns, and parameter threading ## Related Issue(s) Closes #333 ## Type of Change Select all that apply: **Code & Documentation:** - [x] Bug fix (non-breaking change fixing an issue) - [ ] New feature (non-breaking change adding functionality) - [ ] Breaking change (fix or feature causing existing functionality to change) - [ ] Documentation update **Infrastructure & Configuration:** - [x] GitHub Actions workflow - [ ] Linting configuration (markdown, PowerShell, etc.) - [ ] Security configuration - [ ] DevContainer configuration - [ ] Dependency update **AI Artifacts:** - [ ] Reviewed contribution with `prompt-builder` agent and addressed all feedback - [ ] Copilot instructions (`.github/instructions/*.instructions.md`) - [ ] Copilot prompt (`.github/prompts/*.prompt.md`) - [ ] Copilot agent (`.github/agents/*.agent.md`) **Other:** - [x] Script/automation (`.ps1`, `.sh`, `.py`) - [ ] Other (please describe): ## Testing - **Unit tests**: 4 new tests in `FrontmatterValidation.Tests.ps1` covering `Test-SingleFileFrontmatter` footer exclusion behavior - **Integration tests**: 2 new tests in `Validate-MarkdownFrontmatter.Tests.ps1` verifying parameter threading to module - **All tests pass**: 237 total Pester tests execute successfully - **PSScriptAnalyzer**: Clean analysis with no issues ## Checklist ### Required Checks - [x] Documentation is updated (if applicable) - [x] Files follow existing naming conventions - [x] Changes are backwards compatible (if applicable) - [x] Tests added for new functionality (if applicable) ### Required Automated Checks The following validation commands must pass before merging: - [x] Markdown linting: `npm run lint:md` - [x] Spell checking: `npm run spell-check` - [x] Frontmatter validation: `npm run lint:frontmatter` - [x] Link validation: `npm run lint:md-links` - [x] PowerShell analysis: `npm run lint:ps` ## Security Considerations - [x] This PR does not contain any sensitive or NDA information - [x] Any new dependencies have been reviewed for security issues - [x] Security-related scripts follow the principle of least privilege ## Additional Notes The `FooterExcludePaths` parameter uses PowerShell's `-like` operator for pattern matching, supporting wildcards (`*`, `?`). Path separators are normalized to forward slashes for consistent cross-platform behavior. Default workflow configuration excludes `CHANGELOG.md` from footer validation since changelog files follow their own formatting conventions.
1 parent c0e48c6 commit 64db98d

File tree

5 files changed

+165
-6
lines changed

5 files changed

+165
-6
lines changed

.github/workflows/frontmatter-validation.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ on:
2323
required: false
2424
type: boolean
2525
default: true
26+
footer-exclude-paths:
27+
description: 'Comma-separated list of file patterns to exclude from footer validation'
28+
required: false
29+
type: string
30+
default: 'CHANGELOG.md'
2631

2732
permissions:
2833
contents: read
@@ -65,6 +70,12 @@ jobs:
6570
$params['WarningsAsErrors'] = $true
6671
}
6772
73+
# Parse footer exclude paths from comma-separated input
74+
$footerExcludeInput = '${{ inputs.footer-exclude-paths }}'
75+
if ($footerExcludeInput -and $footerExcludeInput -ne '') {
76+
$params['FooterExcludePaths'] = $footerExcludeInput -split ',' | ForEach-Object { $_.Trim() }
77+
}
78+
6879
# Exclude test fixture files (intentionally invalid frontmatter for testing)
6980
$params['ExcludePaths'] = @('scripts/tests/Fixtures/**')
7081

scripts/linting/Modules/FrontmatterValidation.psm1

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -783,6 +783,10 @@ function Test-SingleFileFrontmatter {
783783
Repository root path for relative path computation and file classification.
784784
.PARAMETER FileReader
785785
Optional scriptblock for reading file content. Enables testing.
786+
.PARAMETER FooterExcludePaths
787+
Array of wildcard patterns for files to exclude from footer validation.
788+
Uses PowerShell -like operator for matching against relative paths.
789+
Path separators are normalized to forward slashes for cross-platform support.
786790
.OUTPUTS
787791
FileValidationResult
788792
#>
@@ -799,6 +803,8 @@ function Test-SingleFileFrontmatter {
799803

800804
[scriptblock]$FileReader = { param($p) Get-Content -Path $p -Raw -ErrorAction Stop },
801805

806+
[string[]]$FooterExcludePaths = @(),
807+
802808
[switch]$SkipFooterValidation
803809
)
804810

@@ -891,8 +897,20 @@ function Test-SingleFileFrontmatter {
891897
}
892898
}
893899

900+
# Check if file matches footer exclusion pattern
901+
# Normalize path separators for cross-platform pattern matching
902+
$skipFooterForFile = $false
903+
$normalizedRelativePath = $relativePath -replace '\\', '/'
904+
foreach ($pattern in $FooterExcludePaths) {
905+
$normalizedPattern = $pattern -replace '\\', '/'
906+
if ($normalizedRelativePath -like $normalizedPattern) {
907+
$skipFooterForFile = $true
908+
break
909+
}
910+
}
911+
894912
# Footer validation for all markdown EXCEPT AI artifacts (prompts, instructions, agents, chatmodes)
895-
if (-not $isAiArtifact -and -not $SkipFooterValidation) {
913+
if (-not $isAiArtifact -and -not $SkipFooterValidation -and -not $skipFooterForFile) {
896914
# Determine severity based on file type
897915
$footerSeverity = 'Warning'
898916
if ($fileTypeInfo.IsRootCommunityFile -or $fileTypeInfo.IsDevContainer -or $fileTypeInfo.IsVSCodeReadme) {
@@ -924,6 +942,11 @@ function Invoke-FrontmatterValidation {
924942
.PARAMETER RepoRoot
925943
Repository root path for relative path computation and file classification.
926944
945+
.PARAMETER FooterExcludePaths
946+
Array of wildcard patterns for files to exclude from footer validation.
947+
Uses PowerShell -like operator for matching against relative paths.
948+
Path separators are normalized to forward slashes for cross-platform support.
949+
927950
.OUTPUTS
928951
ValidationSummary
929952
#>
@@ -937,13 +960,15 @@ function Invoke-FrontmatterValidation {
937960
[ValidateNotNullOrEmpty()]
938961
[string]$RepoRoot,
939962

963+
[string[]]$FooterExcludePaths = @(),
964+
940965
[switch]$SkipFooterValidation
941966
)
942967

943968
$summary = [ValidationSummary]::new()
944969

945970
foreach ($file in $Files) {
946-
$result = Test-SingleFileFrontmatter -FilePath $file -RepoRoot $RepoRoot -SkipFooterValidation:$SkipFooterValidation
971+
$result = Test-SingleFileFrontmatter -FilePath $file -RepoRoot $RepoRoot -FooterExcludePaths $FooterExcludePaths -SkipFooterValidation:$SkipFooterValidation
947972
$summary.AddResult($result)
948973
}
949974

scripts/linting/Validate-MarkdownFrontmatter.ps1

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ param(
3939
[Parameter(Mandatory = $false)]
4040
[switch]$EnableSchemaValidation,
4141

42+
[Parameter(Mandatory = $false)]
43+
[string[]]$FooterExcludePaths = @(),
44+
4245
[Parameter(Mandatory = $false)]
4346
[switch]$SkipFooterValidation
4447
)
@@ -464,6 +467,11 @@ function Test-FrontmatterValidation {
464467
.PARAMETER EnableSchemaValidation
465468
Enable JSON Schema validation (advisory only).
466469
470+
.PARAMETER FooterExcludePaths
471+
Array of wildcard patterns for files to exclude from footer validation only.
472+
Uses PowerShell -like operator for matching against relative paths.
473+
Path separators are normalized to forward slashes for cross-platform support.
474+
467475
.OUTPUTS
468476
ValidationSummary from FrontmatterValidation module.
469477
#>
@@ -476,6 +484,7 @@ function Test-FrontmatterValidation {
476484
[switch]$ChangedFilesOnly,
477485
[string]$BaseBranch = "origin/main",
478486
[switch]$EnableSchemaValidation,
487+
[string[]]$FooterExcludePaths = @(),
479488
[switch]$SkipFooterValidation
480489
)
481490

@@ -550,7 +559,7 @@ function Test-FrontmatterValidation {
550559
Write-Host "Found $($resolvedFiles.Count) total markdown files to validate" -ForegroundColor Cyan
551560

552561
# Use module's orchestration function for core validation
553-
$summary = Invoke-FrontmatterValidation -Files $resolvedFiles -RepoRoot $repoRoot -SkipFooterValidation:$SkipFooterValidation
562+
$summary = Invoke-FrontmatterValidation -Files $resolvedFiles -RepoRoot $repoRoot -FooterExcludePaths $FooterExcludePaths -SkipFooterValidation:$SkipFooterValidation
554563

555564
# Optional schema validation overlay (advisory only)
556565
# Uses frontmatter already parsed by Invoke-FrontmatterValidation
@@ -726,13 +735,13 @@ function Get-ChangedMarkdownFileGroup {
726735
try {
727736
if ($MyInvocation.InvocationName -ne '.') {
728737
if ($ChangedFilesOnly) {
729-
$result = Test-FrontmatterValidation -ChangedFilesOnly -BaseBranch $BaseBranch -ExcludePaths $ExcludePaths -WarningsAsErrors:$WarningsAsErrors -EnableSchemaValidation:$EnableSchemaValidation -SkipFooterValidation:$SkipFooterValidation
738+
$result = Test-FrontmatterValidation -ChangedFilesOnly -BaseBranch $BaseBranch -ExcludePaths $ExcludePaths -WarningsAsErrors:$WarningsAsErrors -EnableSchemaValidation:$EnableSchemaValidation -FooterExcludePaths $FooterExcludePaths -SkipFooterValidation:$SkipFooterValidation
730739
}
731740
elseif ($Files.Count -gt 0) {
732-
$result = Test-FrontmatterValidation -Files $Files -ExcludePaths $ExcludePaths -WarningsAsErrors:$WarningsAsErrors -EnableSchemaValidation:$EnableSchemaValidation -SkipFooterValidation:$SkipFooterValidation
741+
$result = Test-FrontmatterValidation -Files $Files -ExcludePaths $ExcludePaths -WarningsAsErrors:$WarningsAsErrors -EnableSchemaValidation:$EnableSchemaValidation -FooterExcludePaths $FooterExcludePaths -SkipFooterValidation:$SkipFooterValidation
733742
}
734743
else {
735-
$result = Test-FrontmatterValidation -Paths $Paths -ExcludePaths $ExcludePaths -WarningsAsErrors:$WarningsAsErrors -EnableSchemaValidation:$EnableSchemaValidation -SkipFooterValidation:$SkipFooterValidation
744+
$result = Test-FrontmatterValidation -Paths $Paths -ExcludePaths $ExcludePaths -WarningsAsErrors:$WarningsAsErrors -EnableSchemaValidation:$EnableSchemaValidation -FooterExcludePaths $FooterExcludePaths -SkipFooterValidation:$SkipFooterValidation
736745
}
737746

738747
# Normalize result: if pipeline output produced an array, extract the ValidationSummary object

scripts/tests/linting/FrontmatterValidation.Tests.ps1

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,74 @@ description: Test
13101310
$result.RelativePath | Should -Be $expectedPath
13111311
}
13121312
}
1313+
1314+
Context 'Footer exclude paths' {
1315+
It 'Skips footer validation for file matching exclusion pattern' {
1316+
$mockContent = @"
1317+
---
1318+
title: Changelog
1319+
description: Release history
1320+
---
1321+
1322+
# Changelog
1323+
1324+
No Copilot footer here
1325+
"@
1326+
$testFile = Join-Path $script:TestRepoRoot 'CHANGELOG.md'
1327+
$result = Test-SingleFileFrontmatter `
1328+
-FilePath $testFile `
1329+
-RepoRoot $script:TestRepoRoot `
1330+
-FooterExcludePaths @('CHANGELOG.md') `
1331+
-FileReader { $mockContent }.GetNewClosure()
1332+
1333+
# File without footer should NOT have footer error when excluded
1334+
$footerIssues = $result.Issues | Where-Object { $_.Field -eq 'footer' }
1335+
$footerIssues | Should -BeNullOrEmpty
1336+
}
1337+
1338+
It 'Applies footer validation for non-excluded files' {
1339+
$mockContent = @"
1340+
---
1341+
title: Test Doc
1342+
description: Test description
1343+
---
1344+
1345+
# Content
1346+
1347+
No Copilot footer here
1348+
"@
1349+
$testFile = Join-Path $script:TestRepoRoot 'docs' 'guide.md'
1350+
$result = Test-SingleFileFrontmatter `
1351+
-FilePath $testFile `
1352+
-RepoRoot $script:TestRepoRoot `
1353+
-FooterExcludePaths @('CHANGELOG.md') `
1354+
-FileReader { $mockContent }.GetNewClosure()
1355+
1356+
# Non-excluded file without footer should have footer error
1357+
$footerIssues = $result.Issues | Where-Object { $_.Field -eq 'footer' }
1358+
$footerIssues | Should -Not -BeNullOrEmpty
1359+
}
1360+
1361+
It 'Supports wildcard patterns in exclusions' {
1362+
$mockContent = @"
1363+
---
1364+
title: Test
1365+
description: Test
1366+
---
1367+
1368+
No footer
1369+
"@
1370+
$testFile = Join-Path $script:TestRepoRoot 'logs' 'output.md'
1371+
$result = Test-SingleFileFrontmatter `
1372+
-FilePath $testFile `
1373+
-RepoRoot $script:TestRepoRoot `
1374+
-FooterExcludePaths @('logs/*.md') `
1375+
-FileReader { $mockContent }.GetNewClosure()
1376+
1377+
$footerIssues = $result.Issues | Where-Object { $_.Field -eq 'footer' }
1378+
$footerIssues | Should -BeNullOrEmpty
1379+
}
1380+
}
13131381
}
13141382

13151383
Describe 'Invoke-FrontmatterValidation' -Tag 'Unit' {
@@ -1437,6 +1505,29 @@ Describe 'Invoke-FrontmatterValidation' -Tag 'Unit' {
14371505
$summary.TotalFiles | Should -Be 1
14381506
}
14391507
}
1508+
1509+
Context 'FooterExcludePaths threading' {
1510+
It 'Passes FooterExcludePaths to Test-SingleFileFrontmatter' {
1511+
$capturedParams = @{}
1512+
1513+
Mock Test-SingleFileFrontmatter -ModuleName FrontmatterValidation {
1514+
$capturedParams.FooterExcludePaths = $FooterExcludePaths
1515+
& (Get-Module FrontmatterValidation) {
1516+
param($path)
1517+
$r = [FileValidationResult]::new($path)
1518+
$r.HasFrontmatter = $true
1519+
return $r
1520+
} $FilePath
1521+
}
1522+
1523+
$null = Invoke-FrontmatterValidation `
1524+
-Files @("$script:TestRepoRoot\file.md") `
1525+
-RepoRoot $script:TestRepoRoot `
1526+
-FooterExcludePaths @('CHANGELOG.md', 'logs/*.md')
1527+
1528+
$capturedParams.FooterExcludePaths | Should -Be @('CHANGELOG.md', 'logs/*.md')
1529+
}
1530+
}
14401531
}
14411532

14421533
#endregion

scripts/tests/linting/Validate-MarkdownFrontmatter.Tests.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1055,6 +1055,29 @@ Content
10551055
$relativePath -like $pattern | Should -BeTrue
10561056
}
10571057
}
1058+
1059+
Context 'FooterExcludePaths integration' {
1060+
It 'Passes FooterExcludePaths to Invoke-FrontmatterValidation' {
1061+
$testFile = Join-Path $TestDrive 'CHANGELOG.md'
1062+
Set-Content $testFile "---`ndescription: Release history`n---`n# Changelog`n`nNo footer here"
1063+
1064+
# File should not have footer error when excluded (use wildcard to match filename in any path)
1065+
$result = Test-FrontmatterValidation -Files @($testFile) -FooterExcludePaths @('*CHANGELOG.md')
1066+
$footerErrors = $result.Results | ForEach-Object { $_.Issues } | Where-Object { $_.Field -eq 'footer' }
1067+
$footerErrors | Should -BeNullOrEmpty
1068+
}
1069+
1070+
It 'Applies footer validation to non-excluded files' {
1071+
$testFile = Join-Path $TestDrive 'docs' 'guide.md'
1072+
New-Item -ItemType Directory -Path (Join-Path $TestDrive 'docs') -Force | Out-Null
1073+
Set-Content $testFile "---`ndescription: Test guide`n---`n# Guide`n`nNo footer here"
1074+
1075+
# Non-excluded file should have footer error
1076+
$result = Test-FrontmatterValidation -Files @($testFile) -FooterExcludePaths @('*CHANGELOG.md')
1077+
$footerErrors = $result.Results | ForEach-Object { $_.Issues } | Where-Object { $_.Field -eq 'footer' }
1078+
$footerErrors | Should -Not -BeNullOrEmpty
1079+
}
1080+
}
10581081
}
10591082

10601083
#endregion

0 commit comments

Comments
 (0)