Files
2026-08-28 11:11:17 +02:00

554 lines
23 KiB
PowerShell

#!/usr/bin/env pwsh
<#
.SYNOPSIS
Git pre-commit hook to check for COM object leaks, Core Commands coverage, naming consistency, Success flag violations, CLI workflow, MCP Server functionality, and release deliverables
.DESCRIPTION
Runs checks before allowing commits:
0. Process cleanup - kills stale Excel, excelcli, and MCP server processes to prevent file locks
1. COM leak checker - ensures no Excel COM objects are leaked
2. Coverage and naming audit - ensures 100% Core Commands are exposed via MCP Server with aligned action names
3. MCP-Core implementation audit - ensures every MCP action still has a Core implementation
4. Success flag validation - ensures Success=true never paired with ErrorMessage (Rule 0)
5. Release solution build - generates Release binaries and skill outputs used by downstream packaging (skipped for docs-only commits)
5b. Documentation count validation - ensures all docs report the code-derived tool/operation counts (skipped for docs-only commits)
6. CLI workflow smoke test - validates end-to-end CLI functionality (skipped for docs/changeset-only commits)
7. MCP Server smoke test - validates all MCP tools work correctly (skipped for docs/changeset-only commits)
8. CLI release packaging - validates NuGet + standalone ZIP artifacts (skipped for docs-only commits)
9. MCP Server release packaging - validates NuGet + standalone ZIP artifacts (skipped for docs-only commits)
10. VS Code extension packaging - validates the VSIX release packaging path (skipped for docs-only commits)
11. MCPB bundle packaging - validates the Claude Desktop bundle artifact (skipped for docs-only commits)
12. Agent skills packaging - validates the ZIP deliverable (skipped for docs-only commits)
13. Plugin README validation - ensures overlays are complete and not stub content
14. Dynamic cast audit - ensures ((dynamic)) casts are documented
Ensures code quality and prevents regression.
.EXAMPLE
.\pre-commit.ps1
.NOTES
This script is called by the Git pre-commit hook.
To install: Copy .git/hooks/pre-commit (bash) or configure Git to use this PowerShell version.
#>
$ErrorActionPreference = "Stop"
$rootDir = Split-Path -Parent $PSScriptRoot
$preCommitArtifactsDir = Join-Path $rootDir "artifacts\pre-commit"
$nugetConfigPath = Join-Path $rootDir "NuGet.Config"
$version = $null
function Invoke-ValidationStep {
param(
[string]$Heading,
[scriptblock]$Action,
[string]$FailureSummary,
[string]$SuccessSummary
)
Write-Host ""
Write-Host $Heading -ForegroundColor Cyan
try {
$output = & $Action 2>&1 | Out-String
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
Write-Host ""
Write-Host $FailureSummary -ForegroundColor Red
if (-not [string]::IsNullOrWhiteSpace($output)) {
Write-Host ""
Write-Host $output -ForegroundColor Gray
}
exit 1
}
Write-Host $SuccessSummary -ForegroundColor Green
}
catch {
Write-Host ""
Write-Host "$FailureSummary $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
}
function Reset-Directory {
param([string]$Path)
if (Test-Path $Path) {
Remove-Item $Path -Recurse -Force
}
New-Item -ItemType Directory -Path $Path -Force | Out-Null
}
function Stop-DotNetBuildServers {
dotnet build-server shutdown *> $null
}
# Determine whether this commit touches actual code (as opposed to docs/changeset-only
# changes). The Release build, smoke tests and release packaging gates all exercise
# compiled binaries and are slow (minutes) - they add no value for pure documentation
# changes, including edits to the gh-pages documentation website and its star-history
# generation workflow. These files do not affect the shipped Excel binaries. Cheap
# source-level guards still run for every commit.
$docOnlyPattern = '(\.md$)|(^\.changeset/)|(^docs/)|(^gh-pages/)|(^\.github/(ISSUE_TEMPLATE|PULL_REQUEST_TEMPLATE))|(^\.github/workflows/deploy-gh-pages\.yml$)|(^scripts/(pre-commit|(Update|Restore|Persist|Test)-StarHistory)\.ps1$)'
$mergeHead = git rev-parse --verify --quiet MERGE_HEAD 2>$null
$validationBase = if ($LASTEXITCODE -eq 0 -and $mergeHead) { $mergeHead } else { "HEAD" }
$stagedFiles = git diff --cached --name-only $validationBase 2>&1 | Where-Object { $_ }
$codeChangedFiles = $stagedFiles | Where-Object { $_ -notmatch $docOnlyPattern }
$hasCodeChanges = @($codeChangedFiles).Count -gt 0
# Excel-dependent E2E validates the runtime path only. Include the COM and service
# layers plus source generators because their changes flow into Core, CLI, or MCP.
$excelE2EPattern = '(^src/ExcelMcp\.(CLI|ComInterop|Core|McpServer|Service)/)|(^src/ExcelMcp\.Generators(\.[^/]+)?/)|(^scripts/Test-E2E\.ps1$)'
$excelE2EChangedFiles = $stagedFiles | Where-Object { $_ -match $excelE2EPattern }
$requiresExcelE2E = @($excelE2EChangedFiles).Count -gt 0
# CRITICAL: Check branch FIRST - never commit directly to main (Rule 6)
Write-Host "Checking current branch..." -ForegroundColor Cyan
$currentBranch = git branch --show-current
if ($currentBranch -eq "main") {
Write-Host ""
Write-Host "BLOCKED: Cannot commit directly to 'main' branch!" -ForegroundColor Red
Write-Host ""
Write-Host " Rule 6: All Changes Via Pull Requests" -ForegroundColor Yellow
Write-Host " 'Never commit to main. Create feature branch -> PR -> CI/CD + review -> merge.'" -ForegroundColor Yellow
Write-Host ""
Write-Host " To fix:" -ForegroundColor Cyan
Write-Host " 1. git stash # Save your changes" -ForegroundColor White
Write-Host " 2. git checkout -b feature/your-feature-name # Create feature branch" -ForegroundColor White
Write-Host " 3. git stash pop # Restore changes" -ForegroundColor White
Write-Host " 4. git add <files> # Stage changes" -ForegroundColor White
Write-Host " 5. git commit -m 'your message' # Commit to feature branch" -ForegroundColor White
Write-Host ""
exit 1
}
Write-Host "Branch check passed - on '$currentBranch' (not main)" -ForegroundColor Green
Write-Host ""
# Stop only processes owned by the selected CLI pipe before touching Release binaries.
Write-Host "Stopping pipe-owned ExcelMCP processes..." -ForegroundColor Cyan
$ownedCleanupScript = Join-Path $PSScriptRoot "Stop-ExcelMcpProcesses.ps1"
& $ownedCleanupScript -Verbose
if ($LASTEXITCODE -ne 0) {
Write-Host "Pipe-owned cleanup failed with exit code $LASTEXITCODE." -ForegroundColor Red
exit 1
}
Stop-DotNetBuildServers
Write-Host "Process cleanup done" -ForegroundColor Green
Write-Host ""
Reset-Directory -Path $preCommitArtifactsDir
try {
$propsPath = Join-Path $rootDir "Directory.Build.props"
$propsXml = [xml](Get-Content $propsPath)
$version = $propsXml.Project.PropertyGroup.Version | Where-Object { $_ } | Select-Object -First 1
}
catch {
Write-Host "Warning: Could not read version from Directory.Build.props ($($_.Exception.Message))" -ForegroundColor Yellow
$version = "local"
}
Write-Host "Checking for COM object leaks..." -ForegroundColor Cyan
try {
$leakCheckScript = Join-Path $rootDir "scripts\check-com-leaks.ps1"
& $leakCheckScript
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "COM object leaks detected! Fix them before committing." -ForegroundColor Red
exit 1
}
Write-Host "COM leak check passed" -ForegroundColor Green
}
catch {
Write-Host "Error running COM leak check: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Host " Continuing with coverage audit..." -ForegroundColor Gray
}
Write-Host ""
Write-Host "Checking Core Commands coverage and naming..." -ForegroundColor Cyan
try {
$auditScript = Join-Path $rootDir "scripts\audit-core-coverage.ps1"
& $auditScript -CheckNaming -FailOnGaps
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "Coverage or naming issues detected!" -ForegroundColor Red
Write-Host " All Core methods must be exposed via MCP Server with matching names." -ForegroundColor Red
Write-Host " Fix the issues before committing (add/rename enum values and mappings)." -ForegroundColor Red
exit 1
}
Write-Host "Coverage and naming checks passed - 100% coverage with consistent names" -ForegroundColor Green
}
catch {
Write-Host ""
Write-Host "Error running coverage audit: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "Checking MCP actions have Core implementations..." -ForegroundColor Cyan
try {
$mcpCoreScript = Join-Path $rootDir "scripts\check-mcp-core-implementations.ps1"
& $mcpCoreScript
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "MCP actions without Core implementations detected!" -ForegroundColor Red
Write-Host " All enum actions must have matching Core Command methods." -ForegroundColor Red
Write-Host " Fix the issues before committing (remove enum or implement method)." -ForegroundColor Red
exit 1
}
Write-Host "MCP-Core implementation check passed" -ForegroundColor Green
}
catch {
Write-Host ""
Write-Host "Error running MCP-Core implementation check: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "Checking Success flag violations (Rule 0)..." -ForegroundColor Cyan
try {
$successFlagScript = Join-Path $rootDir "scripts\check-success-flag.ps1"
& $successFlagScript
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "Success flag violations detected!" -ForegroundColor Red
Write-Host " CRITICAL: Success=true with ErrorMessage confuses LLMs and causes data corruption." -ForegroundColor Red
Write-Host " Fix the violations before committing (add Success=false in catch blocks)." -ForegroundColor Red
exit 1
}
Write-Host "Success flag check passed - all flags match reality" -ForegroundColor Green
}
catch {
Write-Host ""
Write-Host "Error running success flag check: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
Invoke-ValidationStep `
-Heading "Checking shared plugin bootstrap template drift..." `
-FailureSummary "Plugin bootstrap drift detected. Regenerate the source-copied download.ps1 files with scripts/Build-BootstrapScripts.ps1." `
-SuccessSummary "Shared bootstrap template check passed - the canonical render matches both committed copies" `
-Action {
$bootstrapScript = Join-Path $rootDir "scripts\Build-BootstrapScripts.ps1"
& $bootstrapScript -Check
}
# NOTE: CLI coverage checks removed - commands are now auto-generated by Roslyn source generators
# The CLI generator produces all command classes and registration from Core interfaces
# Validation is handled by:
# - Build-time generator errors if interfaces are malformed
# - CLI workflow smoke test below (end-to-end validation)
# Everything from the Release build through the packaging gates exercises compiled
# binaries and release artifacts. For a docs-only commit (including gh-pages website
# changes) there is no compiled surface to validate, so skip the whole block and keep
# the commit fast. The cheap source-level guards above and below still run every time.
if ($hasCodeChanges) {
Invoke-ValidationStep `
-Heading "Building Release solution..." `
-FailureSummary "Release solution build failed!" `
-SuccessSummary "Release solution build passed - Release binaries and generated skill docs are up to date" `
-Action {
Push-Location $rootDir
try {
dotnet build Sbroenne.ExcelMcp.sln --configuration Release --configfile $nugetConfigPath -p:NuGetAudit=false --verbosity minimal
}
finally {
Pop-Location
}
}
Write-Host ""
Write-Host "Auto-staging generated SKILL.md files..." -ForegroundColor Cyan
try {
# SKILL.md + references are generated during the Release solution build above.
# Auto-stage all of them so developers never have to think about it.
$skillPaths = @(
"skills/excel-mcp/SKILL.md",
"skills/excel-cli/SKILL.md",
"skills/excel-mcp/references/",
"skills/excel-cli/references/"
)
$skillDiff = git diff --name-only -- @skillPaths 2>&1
$untrackedSkills = git ls-files --others --exclude-standard -- @skillPaths 2>&1
$allChanges = @()
if ($skillDiff) { $allChanges += $skillDiff }
if ($untrackedSkills) { $allChanges += $untrackedSkills }
if ($allChanges.Count -gt 0) {
git add -- @skillPaths
Write-Host "Skill files were regenerated and auto-staged ($($allChanges.Count) files)" -ForegroundColor Green
$allChanges | ForEach-Object { Write-Host " + $_" -ForegroundColor DarkGray }
} else {
Write-Host "Skill files are already up to date" -ForegroundColor Green
}
}
catch {
Write-Host "Error auto-staging SKILL.md files: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Host " Continuing with remaining checks..." -ForegroundColor Gray
}
Invoke-ValidationStep `
-Heading "Validating documentation tool/operation counts..." `
-FailureSummary "Documentation count validation failed! A doc advertises a tool/operation count that does not match the code-derived canonical count." `
-SuccessSummary "Documentation count validation passed - all docs match the canonical counts" `
-Action {
$docCountScript = Join-Path $rootDir "scripts\check-doc-counts.ps1"
& $docCountScript -SkipBuild
}
if ($requiresExcelE2E) {
Invoke-ValidationStep `
-Heading "Running Excel-dependent E2E tests..." `
-FailureSummary "Excel-dependent E2E tests failed! Both the CLI workflow and MCP all-tools smoke tests must pass." `
-SuccessSummary "Excel-dependent E2E tests passed" `
-Action {
$e2eScript = Join-Path $rootDir "scripts\Test-E2E.ps1"
& $e2eScript -SkipBuild
}
} else {
Write-Host ""
Write-Host "Skipping Excel-dependent E2E tests (no staged changes affect Core, CLI, or MCP runtime paths)" -ForegroundColor Yellow
}
Invoke-ValidationStep `
-Heading "Building CLI release deliverables..." `
-FailureSummary "CLI release deliverable validation failed!" `
-SuccessSummary "CLI release deliverables passed - NuGet package and standalone ZIP were built locally" `
-Action {
$cliNupkgDir = Join-Path $preCommitArtifactsDir "cli-nupkg"
$cliPublishDir = Join-Path $preCommitArtifactsDir "cli-publish"
$cliReleaseDir = Join-Path $preCommitArtifactsDir "cli-release"
$cliZipPath = Join-Path $preCommitArtifactsDir "ExcelMcp-CLI-$version-windows.zip"
Reset-Directory -Path $cliNupkgDir
Reset-Directory -Path $cliPublishDir
Reset-Directory -Path $cliReleaseDir
Stop-DotNetBuildServers
Push-Location $rootDir
try {
dotnet pack src\ExcelMcp.CLI\ExcelMcp.CLI.csproj --configuration Release --no-build --no-restore --output $cliNupkgDir -p:Version=$version -p:NuGetAudit=false
dotnet publish src\ExcelMcp.CLI\ExcelMcp.CLI.csproj --configuration Release --runtime win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=false -p:PublishReadyToRun=false -p:Version=$version -p:NuGetAudit=false --output $cliPublishDir
Copy-Item (Join-Path $cliPublishDir "excelcli.exe") $cliReleaseDir
Copy-Item "README.md" $cliReleaseDir
Copy-Item "LICENSE" $cliReleaseDir
if (Test-Path $cliZipPath) {
Remove-Item $cliZipPath -Force
}
Compress-Archive -Path (Join-Path $cliReleaseDir "*") -DestinationPath $cliZipPath
if (-not (Get-ChildItem $cliNupkgDir -Filter "*.nupkg" -ErrorAction Stop)) {
throw "CLI NuGet package was not created."
}
if (-not (Test-Path $cliZipPath)) {
throw "CLI ZIP artifact was not created."
}
}
finally {
Pop-Location
}
}
Invoke-ValidationStep `
-Heading "Building MCP Server release deliverables..." `
-FailureSummary "MCP Server release deliverable validation failed!" `
-SuccessSummary "MCP Server release deliverables passed - NuGet package and standalone ZIP were built locally" `
-Action {
$mcpNupkgDir = Join-Path $preCommitArtifactsDir "mcp-server-nupkg"
$mcpPublishDir = Join-Path $preCommitArtifactsDir "mcp-server-publish"
$mcpReleaseDir = Join-Path $preCommitArtifactsDir "mcp-server-release"
$mcpZipPath = Join-Path $preCommitArtifactsDir "ExcelMcp-MCP-Server-$version-windows.zip"
Reset-Directory -Path $mcpNupkgDir
Reset-Directory -Path $mcpPublishDir
Reset-Directory -Path $mcpReleaseDir
Stop-DotNetBuildServers
Push-Location $rootDir
try {
dotnet pack src\ExcelMcp.McpServer\ExcelMcp.McpServer.csproj --configuration Release --no-build --no-restore --output $mcpNupkgDir -p:Version=$version -p:NuGetAudit=false
dotnet publish src\ExcelMcp.McpServer\ExcelMcp.McpServer.csproj --configuration Release --runtime win-x64 --self-contained true -p:PublishSingleFile=true -p:IncludeNativeLibrariesForSelfExtract=true -p:PublishTrimmed=false -p:PublishReadyToRun=false -p:Version=$version -p:NuGetAudit=false --output $mcpPublishDir
$publishedExe = Join-Path $mcpPublishDir "Sbroenne.ExcelMcp.McpServer.exe"
$renamedExe = Join-Path $mcpPublishDir "mcp-excel.exe"
if (-not (Test-Path $publishedExe)) {
throw "Published MCP Server executable was not created."
}
if (Test-Path $renamedExe) {
Remove-Item $renamedExe -Force
}
Rename-Item $publishedExe "mcp-excel.exe"
Copy-Item (Join-Path $mcpPublishDir "mcp-excel.exe") $mcpReleaseDir
Copy-Item "README.md" $mcpReleaseDir
Copy-Item "LICENSE" $mcpReleaseDir
if (Test-Path $mcpZipPath) {
Remove-Item $mcpZipPath -Force
}
Compress-Archive -Path (Join-Path $mcpReleaseDir "*") -DestinationPath $mcpZipPath
if (-not (Get-ChildItem $mcpNupkgDir -Filter "*.nupkg" -ErrorAction Stop)) {
throw "MCP Server NuGet package was not created."
}
if (-not (Test-Path $mcpZipPath)) {
throw "MCP Server ZIP artifact was not created."
}
}
finally {
Pop-Location
}
}
Invoke-ValidationStep `
-Heading "Running VS Code extension package validation..." `
-FailureSummary "VS Code extension package validation failed! Fix the extension build or manifest mismatch before committing." `
-SuccessSummary "VS Code extension package validation passed" `
-Action {
$extensionDir = Join-Path $rootDir "vscode-extension"
$packageLog = Join-Path $preCommitArtifactsDir "vscode-package.log"
Push-Location $extensionDir
try {
& $env:ComSpec /d /s /c "npm run package > `"$packageLog`" 2>&1"
$packageExitCode = $LASTEXITCODE
if ($packageExitCode -ne 0) {
Get-Content -LiteralPath $packageLog
throw "npm run package failed with exit code $packageExitCode"
}
}
finally {
Pop-Location
}
}
Invoke-ValidationStep `
-Heading "Building MCPB bundle deliverable..." `
-FailureSummary "MCPB bundle validation failed!" `
-SuccessSummary "MCPB bundle validation passed - Claude Desktop bundle was built locally" `
-Action {
$mcpbOutputRelative = "..\artifacts\pre-commit\mcpb"
$mcpbOutputDir = Join-Path $preCommitArtifactsDir "mcpb"
Reset-Directory -Path $mcpbOutputDir
$mcpbDir = Join-Path $rootDir "mcpb"
Push-Location $mcpbDir
try {
.\Build-McpBundle.ps1 -Version $version -OutputDir $mcpbOutputRelative
if (-not (Get-ChildItem $mcpbOutputDir -Filter "*.mcpb" -ErrorAction Stop)) {
throw "MCPB artifact was not created."
}
}
finally {
Pop-Location
}
}
Invoke-ValidationStep `
-Heading "Building agent skills deliverables..." `
-FailureSummary "Agent skills deliverable validation failed!" `
-SuccessSummary "Agent skills deliverables passed - ZIP package was built locally" `
-Action {
$skillsOutputDir = Join-Path $preCommitArtifactsDir "skills"
Reset-Directory -Path $skillsOutputDir
Push-Location $rootDir
try {
.\scripts\Build-AgentSkills.ps1 -OutputDir "artifacts/pre-commit/skills" -Version $version
if (-not (Get-ChildItem $skillsOutputDir -Filter "excel-skills-v*.zip" -ErrorAction Stop)) {
throw "Agent skills ZIP artifact was not created."
}
}
finally {
Pop-Location
}
}
}
else {
Write-Host ""
Write-Host "Skipping Release build, smoke tests and all release packaging gates (docs-only commit - no compiled surface changed)" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Validating plugin README overlays..." -ForegroundColor Cyan
try {
$pluginReadmeScript = Join-Path $rootDir "scripts\check-plugin-readmes.ps1"
& $pluginReadmeScript
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "Plugin README validation failed!" -ForegroundColor Red
Write-Host " Thin/stub README overlays would overwrite richer published templates." -ForegroundColor Red
Write-Host " Enrich the overlay or remove it to use the published template." -ForegroundColor Red
exit 1
}
Write-Host "Plugin README validation passed - overlays are complete" -ForegroundColor Green
}
catch {
Write-Host "Error running plugin README check: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Host " Continuing..." -ForegroundColor Gray
}
Write-Host ""
Write-Host "Checking for undocumented ((dynamic)) casts..." -ForegroundColor Cyan
try {
$dynamicCastScript = Join-Path $rootDir "scripts\check-dynamic-casts.ps1"
& $dynamicCastScript
if ($LASTEXITCODE -ne 0) {
Write-Host ""
Write-Host "Undocumented ((dynamic)) casts detected!" -ForegroundColor Red
Write-Host " Add a justification comment (// PIA gap:, // TODO:, or // Reason:) before each cast." -ForegroundColor Red
Write-Host " See docs/PIA-COVERAGE.md for guidance." -ForegroundColor Red
exit 1
}
Write-Host "Dynamic cast check passed - all casts are documented" -ForegroundColor Green
}
catch {
Write-Host "Error running dynamic cast check: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Host " Continuing..." -ForegroundColor Gray
}
Write-Host ""
Write-Host "All pre-commit checks passed!" -ForegroundColor Green
exit 0