Skip to content

Commit 92fce72

Browse files
littleKitchenkatriendgWilliamBerryiii
authored
feat: add PowerShell script to validate copyright headers (#370)
## Summary Fixes #306 Adds `Test-CopyrightHeaders.ps1` script that validates the presence of copyright and SPDX license headers in source files. ## Changes ### New Files - `scripts/linting/Test-CopyrightHeaders.ps1` - Main validation script - `scripts/tests/linting/Test-CopyrightHeaders.Tests.ps1` - Pester tests ### Modified Files - `package.json` - Added `validate:copyright` npm script ## Features - Scans `.ps1`, `.psm1`, `.psd1`, `.sh` files by default - Checks for `# Copyright (c) Microsoft Corporation.` line - Checks for `# SPDX-License-Identifier: MIT` line - Handles shebang and `#Requires` statement positioning (checks first 15 lines) - Outputs JSON results matching existing format - Supports `-FailOnMissing` flag for CI integration - Includes compliance percentage calculation ## Testing Pester tests cover: - Files with valid headers - Files missing copyright line - Files missing SPDX line - Files missing both headers - Files with #Requires statements - Parameter validation - Output format validation --------- Co-authored-by: Katrien De Graeve <[email protected]> Co-authored-by: Bill Berry <[email protected]>
1 parent 6e26282 commit 92fce72

3 files changed

Lines changed: 576 additions & 1 deletion

File tree

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"lint:all": "npm run format:tables && npm run lint:md && npm run lint:ps && npm run lint:yaml && npm run lint:links && npm run lint:frontmatter",
1818
"format:tables": "markdown-table-formatter \"**/*.md\"",
1919
"extension:prepare": "pwsh ./scripts/extension/Prepare-Extension.ps1",
20-
"extension:package": "pwsh ./scripts/extension/Package-Extension.ps1"
20+
"extension:package": "pwsh ./scripts/extension/Package-Extension.ps1",
21+
"validate:copyright": "pwsh -File scripts/linting/Test-CopyrightHeaders.ps1"
2122
},
2223
"devDependencies": {
2324
"cspell": "9.6.2",
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
#!/usr/bin/env pwsh
2+
# Copyright (c) Microsoft Corporation.
3+
# SPDX-License-Identifier: MIT
4+
<#
5+
.SYNOPSIS
6+
Validates copyright and SPDX license headers in source files.
7+
8+
.DESCRIPTION
9+
Cross-platform PowerShell script that scans source files for required copyright
10+
and SPDX license identifier headers. Integrates with the existing linting
11+
infrastructure and outputs results in JSON format.
12+
13+
.PARAMETER Path
14+
Root path to scan for source files. Defaults to repository root.
15+
16+
.PARAMETER FileExtensions
17+
Array of file extensions to check. Defaults to @('*.ps1', '*.psm1', '*.psd1', '*.sh').
18+
19+
.PARAMETER OutputPath
20+
Path where results should be saved. Defaults to 'logs/copyright-header-results.json'.
21+
22+
.PARAMETER FailOnMissing
23+
Exit with error code if any files are missing required headers. Default is false.
24+
25+
.PARAMETER ExcludePaths
26+
Array of paths to exclude from scanning (supports wildcards).
27+
28+
.EXAMPLE
29+
./Test-CopyrightHeaders.ps1
30+
Scan repository for copyright header compliance.
31+
32+
.EXAMPLE
33+
./Test-CopyrightHeaders.ps1 -FailOnMissing
34+
Scan and fail if any files are missing headers.
35+
36+
.EXAMPLE
37+
./Test-CopyrightHeaders.ps1 -Path "./scripts" -FileExtensions @('*.ps1')
38+
Scan only PowerShell files in scripts directory.
39+
40+
.NOTES
41+
Requires PowerShell 7.0 or later for cross-platform compatibility.
42+
43+
Expected header format:
44+
- Copyright line: # Copyright (c) Microsoft Corporation.
45+
- SPDX line: # SPDX-License-Identifier: MIT
46+
47+
Headers should appear within the first 10 lines of the file,
48+
accounting for shebang and #Requires statements.
49+
50+
.LINK
51+
https://spdx.dev/ids/
52+
#>
53+
54+
[CmdletBinding()]
55+
param(
56+
[Parameter(Mandatory = $false)]
57+
[string]$Path = (git rev-parse --show-toplevel 2>$null),
58+
59+
[Parameter(Mandatory = $false)]
60+
[string[]]$FileExtensions = @('*.ps1', '*.psm1', '*.psd1', '*.sh'),
61+
62+
[Parameter(Mandatory = $false)]
63+
[string]$OutputPath = "logs/copyright-header-results.json",
64+
65+
[Parameter(Mandatory = $false)]
66+
[switch]$FailOnMissing,
67+
68+
[Parameter(Mandatory = $false)]
69+
[string[]]$ExcludePaths = @('node_modules', '.git', 'vendor', 'logs')
70+
)
71+
72+
# Import shared helpers if available
73+
$helpersPath = Join-Path $PSScriptRoot "Modules/LintingHelpers.psm1"
74+
if (Test-Path $helpersPath) {
75+
Import-Module $helpersPath -Force
76+
}
77+
78+
Write-Host "📄 Validating copyright headers..." -ForegroundColor Cyan
79+
80+
# Header patterns to check
81+
$CopyrightPattern = '^\s*#\s*Copyright\s*\(c\)\s*Microsoft\s+Corporation\.?\s*$'
82+
$SpdxPattern = '^\s*#\s*SPDX-License-Identifier:\s*MIT\s*$'
83+
84+
# Lines to check (accounting for shebang, #Requires, etc.)
85+
$MaxLinesToCheck = 15
86+
87+
function Test-FileHeaders {
88+
[CmdletBinding()]
89+
[OutputType([hashtable])]
90+
param(
91+
[Parameter(Mandatory = $true)]
92+
[string]$FilePath
93+
)
94+
95+
$result = @{
96+
file = $FilePath -replace [regex]::Escape($Path), '' -replace '^[\\/]', ''
97+
hasCopyright = $false
98+
hasSpdx = $false
99+
valid = $false
100+
copyrightLine = $null
101+
spdxLine = $null
102+
}
103+
104+
try {
105+
# Read first N lines of file
106+
$lines = Get-Content -Path $FilePath -TotalCount $MaxLinesToCheck -ErrorAction Stop
107+
108+
for ($i = 0; $i -lt $lines.Count; $i++) {
109+
$line = $lines[$i]
110+
$lineNum = $i + 1
111+
112+
if ($line -match $CopyrightPattern) {
113+
$result.hasCopyright = $true
114+
$result.copyrightLine = $lineNum
115+
}
116+
117+
if ($line -match $SpdxPattern) {
118+
$result.hasSpdx = $true
119+
$result.spdxLine = $lineNum
120+
}
121+
}
122+
123+
$result.valid = $result.hasCopyright -and $result.hasSpdx
124+
}
125+
catch {
126+
Write-Warning "Failed to read file: $FilePath - $_"
127+
$result.error = $_.Exception.Message
128+
}
129+
130+
return $result
131+
}
132+
133+
function Get-FilesToCheck {
134+
[CmdletBinding()]
135+
[OutputType([System.IO.FileInfo[]])]
136+
param(
137+
[string]$RootPath,
138+
[string[]]$Extensions,
139+
[string[]]$Exclude
140+
)
141+
142+
$files = @()
143+
144+
foreach ($ext in $Extensions) {
145+
$found = Get-ChildItem -Path $RootPath -Filter $ext -Recurse -File -ErrorAction SilentlyContinue |
146+
Where-Object {
147+
$filePath = $_.FullName
148+
$excluded = $false
149+
foreach ($excludePath in $Exclude) {
150+
if ($filePath -like "*$excludePath*") {
151+
$excluded = $true
152+
break
153+
}
154+
}
155+
-not $excluded
156+
}
157+
$files += $found
158+
}
159+
160+
return $files | Sort-Object FullName -Unique
161+
}
162+
163+
# Ensure output directory exists
164+
$outputDir = Split-Path -Parent $OutputPath
165+
if ($outputDir -and -not (Test-Path $outputDir)) {
166+
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
167+
}
168+
169+
# Get files to check
170+
Write-Host "Scanning for source files in: $Path" -ForegroundColor Gray
171+
$filesToCheck = Get-FilesToCheck -RootPath $Path -Extensions $FileExtensions -Exclude $ExcludePaths
172+
173+
if ($filesToCheck.Count -eq 0) {
174+
Write-Host "⚠️ No files found matching criteria" -ForegroundColor Yellow
175+
exit 0
176+
}
177+
178+
Write-Host "Found $($filesToCheck.Count) files to check" -ForegroundColor Gray
179+
180+
# Check each file
181+
$results = @()
182+
$filesWithHeaders = 0
183+
$filesMissingHeaders = 0
184+
185+
foreach ($file in $filesToCheck) {
186+
$fileResult = Test-FileHeaders -FilePath $file.FullName
187+
188+
if ($fileResult.valid) {
189+
$filesWithHeaders++
190+
Write-Host "$($fileResult.file)" -ForegroundColor Green
191+
}
192+
else {
193+
$filesMissingHeaders++
194+
$missing = @()
195+
if (-not $fileResult.hasCopyright) { $missing += "copyright" }
196+
if (-not $fileResult.hasSpdx) { $missing += "SPDX" }
197+
Write-Host "$($fileResult.file) (missing: $($missing -join ', '))" -ForegroundColor Red
198+
}
199+
200+
$results += $fileResult
201+
}
202+
203+
# Build output object
204+
$output = @{
205+
timestamp = (Get-Date -Format "o")
206+
totalFiles = $filesToCheck.Count
207+
filesWithHeaders = $filesWithHeaders
208+
filesMissingHeaders = $filesMissingHeaders
209+
compliancePercentage = if ($filesToCheck.Count -gt 0) {
210+
[math]::Round(($filesWithHeaders / $filesToCheck.Count) * 100, 2)
211+
} else { 100 }
212+
results = $results
213+
}
214+
215+
# Write results to file
216+
$output | ConvertTo-Json -Depth 10 | Set-Content -Path $OutputPath -Encoding UTF8
217+
Write-Host "`n📊 Results written to: $OutputPath" -ForegroundColor Cyan
218+
219+
# Summary
220+
Write-Host "`n📋 Summary:" -ForegroundColor Cyan
221+
Write-Host " Total files: $($output.totalFiles)" -ForegroundColor Gray
222+
Write-Host " With headers: $($output.filesWithHeaders)" -ForegroundColor Green
223+
Write-Host " Missing headers: $($output.filesMissingHeaders)" -ForegroundColor $(if ($output.filesMissingHeaders -gt 0) { 'Red' } else { 'Green' })
224+
Write-Host " Compliance: $($output.compliancePercentage)%" -ForegroundColor $(if ($output.compliancePercentage -eq 100) { 'Green' } else { 'Yellow' })
225+
226+
# Exit with error if requested and files are missing headers
227+
if ($FailOnMissing -and $filesMissingHeaders -gt 0) {
228+
Write-Host "`n❌ Validation failed: $filesMissingHeaders file(s) missing required headers" -ForegroundColor Red
229+
exit 1
230+
}
231+
232+
Write-Host "`n✅ Copyright header validation complete" -ForegroundColor Green
233+
exit 0

0 commit comments

Comments
 (0)