From 5965b7933cad74cc17b82764c6db967e39003849 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 09:06:42 -0500 Subject: [PATCH 1/9] feat: add Husky.Net pre-commit hook to sync README badges Update the .NET and PepperDash Essentials badges from TargetFramework and MinimumEssentialsFrameworkVersion. Hooks install on restore. --- .github/scripts/Update-ReadmeBadges.ps1 | 99 +++++++++++++++++++++++++ .husky/pre-commit | 22 ++++++ .husky/task-runner.json | 14 ++++ README.md | 5 ++ dotnet-tools.json | 13 ++++ src/Directory.Build.targets | 5 ++ 6 files changed, 158 insertions(+) create mode 100644 .github/scripts/Update-ReadmeBadges.ps1 create mode 100755 .husky/pre-commit create mode 100644 .husky/task-runner.json create mode 100644 dotnet-tools.json diff --git a/.github/scripts/Update-ReadmeBadges.ps1 b/.github/scripts/Update-ReadmeBadges.ps1 new file mode 100644 index 0000000..e0f4240 --- /dev/null +++ b/.github/scripts/Update-ReadmeBadges.ps1 @@ -0,0 +1,99 @@ +<# +.SYNOPSIS + Syncs the README .NET and PepperDash Essentials badges with the project files. +.PARAMETER Check + Exit 1 if the README badges are stale instead of rewriting them. +.PARAMETER Stage + git add README.md when it was changed. +#> +[CmdletBinding()] +param( + [switch]$Check, + [switch]$Stage +) + +$ErrorActionPreference = 'Stop' +$root = Resolve-Path (Join-Path $PSScriptRoot '..\..') +$srcDir = Join-Path $root 'src' +$readmePath = Join-Path $root 'README.md' +$utf8 = New-Object System.Text.UTF8Encoding($false) + +function Fail($message) { + [Console]::Error.WriteLine("badges: $message") + exit 1 +} + +# Target framework from the 4Series csproj +$csprojFile = Get-ChildItem -Path $srcDir -Filter '*.4Series.csproj' | Select-Object -First 1 +if (-not $csprojFile) { Fail 'no *.4Series.csproj found in src/' } +$csproj = [System.IO.File]::ReadAllText($csprojFile.FullName, $utf8) +$tfmMatch = [regex]::Match($csproj, '\s*([^<\s]+)\s*') +if (-not $tfmMatch.Success) { Fail "TargetFramework not found in $($csprojFile.Name)" } +$tfm = $tfmMatch.Groups[1].Value + +# Minimum Essentials version: highest MinimumEssentialsFrameworkVersion across factories +$minVersions = @() +foreach ($file in Get-ChildItem -Path $srcDir -Filter '*Factory.cs') { + $text = [System.IO.File]::ReadAllText($file.FullName, $utf8) + $found = [regex]::Matches($text, '(?m)^\s*MinimumEssentialsFrameworkVersion\s*=\s*"(\d+(?:\.\d+)+)"\s*;') + foreach ($m in $found) { $minVersions += $m.Groups[1].Value } +} +if ($minVersions.Count -eq 0) { Fail 'MinimumEssentialsFrameworkVersion not found in src/*Factory.cs' } +$minVersion = ($minVersions | Sort-Object { [version]$_ } | Select-Object -Last 1) + +# Warn when the NuGet reference and the factory minimum disagree +$pkgMatch = [regex]::Match($csproj, '= v$minVersion)" + +if ($Stage) { git -C $root add README.md } diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100755 index 0000000..fd85d23 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,22 @@ +#!/bin/sh +. "$(dirname "$0")/_/husky.sh" + +## husky task runner examples ------------------- +## Note : for local installation use 'dotnet' prefix. e.g. 'dotnet husky' + +## run all tasks +#husky run + +### run all tasks with group: 'group-name' +#husky run --group group-name + +## run task with name: 'task-name' +#husky run --name task-name + +## pass hook arguments to task +#husky run --args "$1" "$2" + +## or put your custom commands ------------------- +#echo 'Husky.Net is awesome!' + +dotnet husky run diff --git a/.husky/task-runner.json b/.husky/task-runner.json new file mode 100644 index 0000000..dc5f23f --- /dev/null +++ b/.husky/task-runner.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://alirezanet.github.io/Husky.Net/schema.json", + "tasks": [ + { + "name": "update-readme-badges", + "command": "pwsh", + "args": [ "-NoProfile", "-File", ".github/scripts/Update-ReadmeBadges.ps1", "-Stage" ], + "windows": { + "command": "powershell", + "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ".github/scripts/Update-ReadmeBadges.ps1", "-Stage" ] + } + } + ] +} diff --git a/README.md b/README.md index 9d2df4d..1b1f719 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ ![PepperDash Essentials Pluign Logo](/images/essentials-plugin-blue.png) +![PepperDash Essentials](https://img.shields.io/badge/PepperDash%20Essentials-≥%20v2.12.1-blue) +![.NET](https://img.shields.io/badge/.NET%20Framework-4.7.2-512BD4) +![Crestron](https://img.shields.io/badge/Crestron-4--Series-lightgrey) +![License](https://img.shields.io/badge/license-MIT-green) + # Essentials Plugin Template (c) 2025 ## License diff --git a/dotnet-tools.json b/dotnet-tools.json new file mode 100644 index 0000000..db9b9d8 --- /dev/null +++ b/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "husky": { + "version": "0.9.1", + "commands": [ + "husky" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 3af6067..c920210 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -17,4 +17,9 @@ + + + + + From 69b8c4b41948ae0398fc1c154965f78411ee524c Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 09:10:50 -0500 Subject: [PATCH 2/9] chore: cleanup unused CI scripts Remove GenerateVersionNumber, UpdateAssemblyVersion and ZipBuildOutput scripts. --- .github/scripts/GenerateVersionNumber.ps1 | 45 ----------------------- .github/scripts/UpdateAssemblyVersion.ps1 | 40 -------------------- .github/scripts/ZipBuildOutput.ps1 | 42 --------------------- 3 files changed, 127 deletions(-) delete mode 100644 .github/scripts/GenerateVersionNumber.ps1 delete mode 100644 .github/scripts/UpdateAssemblyVersion.ps1 delete mode 100644 .github/scripts/ZipBuildOutput.ps1 diff --git a/.github/scripts/GenerateVersionNumber.ps1 b/.github/scripts/GenerateVersionNumber.ps1 deleted file mode 100644 index 5d5a3ec..0000000 --- a/.github/scripts/GenerateVersionNumber.ps1 +++ /dev/null @@ -1,45 +0,0 @@ -$latestVersions = $(git tag --merged origin/main) -$latestVersion = [version]"0.0.0" -Foreach ($version in $latestVersions) { - Write-Host $version - try { - if (([version]$version) -ge $latestVersion) { - $latestVersion = $version - Write-Host "Setting latest version to: $latestVersion" - } - } - catch { - Write-Host "Unable to convert $($version). Skipping" - continue; - } -} - -$newVersion = [version]$latestVersion -$phase = "" -$newVersionString = "" -switch -regex ($Env:GITHUB_REF) { - '^refs\/heads\/main*.' { - $newVersionString = "{0}.{1}.{2}" -f $newVersion.Major, $newVersion.Minor, $newVersion.Build - } - '^refs\/heads\/feature\/*.' { - $phase = 'alpha' - $newVersionString = "{0}.{1}.{2}-{3}-{4}" -f $newVersion.Major, $newVersion.Minor, ($newVersion.Build + 1), $phase, $Env:GITHUB_RUN_NUMBER - } - '^refs\/heads\/release\/*.' { - $splitRef = $Env:GITHUB_REF -split "/" - $version = [version]($splitRef[-1] -replace "v", "") - $phase = 'rc' - $newVersionString = "{0}.{1}.{2}-{3}-{4}" -f $version.Major, $version.Minor, $version.Build, $phase, $Env:GITHUB_RUN_NUMBER - } - '^refs\/heads\/dev*.' { - $phase = 'beta' - $newVersionString = "{0}.{1}.{2}-{3}-{4}" -f $newVersion.Major, $newVersion.Minor, ($newVersion.Build + 1), $phase, $Env:GITHUB_RUN_NUMBER - } - '^refs\/heads\/hotfix\/*.' { - $phase = 'hotfix' - $newVersionString = "{0}.{1}.{2}-{3}-{4}" -f $newVersion.Major, $newVersion.Minor, ($newVersion.Build + 1), $phase, $Env:GITHUB_RUN_NUMBER - } -} - - -Write-Output $newVersionString diff --git a/.github/scripts/UpdateAssemblyVersion.ps1 b/.github/scripts/UpdateAssemblyVersion.ps1 deleted file mode 100644 index e52b31e..0000000 --- a/.github/scripts/UpdateAssemblyVersion.ps1 +++ /dev/null @@ -1,40 +0,0 @@ -function Update-SourceVersion { - Param ([string]$Version) - #$fullVersion = $Version - $baseVersion = [regex]::Match($Version, "(\d+.\d+.\d+).*").captures.groups[1].value - $NewAssemblyVersion = ‘AssemblyVersion("‘ + $baseVersion + ‘.*")’ - Write-Output "AssemblyVersion = $NewAssemblyVersion" - $NewAssemblyInformationalVersion = ‘AssemblyInformationalVersion("‘ + $Version + ‘")’ - Write-Output "AssemblyInformationalVersion = $NewAssemblyInformationalVersion" - - foreach ($o in $input) { - Write-output $o.FullName - $TmpFile = $o.FullName + “.tmp” - get-content $o.FullName | - ForEach-Object { - $_ -replace ‘AssemblyVersion\(".*"\)’, $NewAssemblyVersion } | - ForEach-Object { - $_ -replace ‘AssemblyInformationalVersion\(".*"\)’, $NewAssemblyInformationalVersion - } > $TmpFile - move-item $TmpFile $o.FullName -force - } -} - -function Update-AllAssemblyInfoFiles ( $version ) { - foreach ($file in “AssemblyInfo.cs”, “AssemblyInfo.vb” ) { - get-childitem -Path $Env:GITHUB_WORKSPACE -recurse | Where-Object { $_.Name -eq $file } | Update-SourceVersion $version ; - } -} - -# validate arguments -$r = [System.Text.RegularExpressions.Regex]::Match($args[0], "\d+\.\d+\.\d+.*"); -if ($r.Success) { - Write-Output "Updating Assembly Version to $args ..."; - Update-AllAssemblyInfoFiles $args[0]; -} -else { - Write-Output ” “; - Write-Output “Error: Input version does not match x.y.z format!” - Write-Output ” “; - Write-Output "Unable to apply version to AssemblyInfo.cs files"; -} diff --git a/.github/scripts/ZipBuildOutput.ps1 b/.github/scripts/ZipBuildOutput.ps1 deleted file mode 100644 index ca4a9a7..0000000 --- a/.github/scripts/ZipBuildOutput.ps1 +++ /dev/null @@ -1,42 +0,0 @@ -# Uncomment these for local testing -# $Env:GITHUB_WORKSPACE = "C:\Working Directories\PD\essentials" -# $Env:SOLUTION_FILE = "PepperDashEssentials" -# $Env:VERSION = "0.0.0-buildType-test" - -# Sets the root directory for the operation -$destination = "$($Env:GITHUB_HOME)\output" -New-Item -ItemType Directory -Force -Path ($destination) -Get-ChildItem ($destination) -$exclusions = @(git submodule foreach --quiet 'echo $name') -# Trying to get any .json schema files (not currently working) -# Gets any files with the listed extensions. -Get-ChildItem -recurse -Path "$($Env:GITHUB_WORKSPACE)" -include "*.cplz" | ForEach-Object { - $allowed = $true; - # Exclude any files in submodules - foreach ($exclude in $exclusions) { - if ((Split-Path $_.FullName -Parent).contains("$($exclude)")) { - $allowed = $false; - break; - } - } - if ($allowed) { - Write-Host "allowing $($_)" - $_; - } -} | Copy-Item -Destination ($destination) -Force -Write-Host "Getting matching files..." -# Get any files from the output folder that match the following extensions -Get-ChildItem -Path $destination | Where-Object {($_.Extension -eq ".clz") -or ($_.Extension -eq ".cpz" -or ($_.Extension -eq ".cplz"))} | ForEach-Object { - # Replace the extensions with dll and xml and create an array - $filenames = @($($_ -replace "cpz|clz|cplz", "dll"), $($_ -replace "cpz|clz|cplz", "xml")) - Write-Host "Filenames:" - Write-Host $filenames - if ($filenames.length -gt 0) { - # Attempt to get the files and return them to the output directory - Get-ChildItem -Recurse -Path "$($Env:GITHUB_WORKSPACE)" -include $filenames | Copy-Item -Destination ($destination) -Force - } -} -Get-ChildItem -Path $destination\*.cplz | Rename-Item -NewName { "$($_.BaseName)-$($Env:VERSION)$($_.Extension)" } -Compress-Archive -Path $destination -DestinationPath "$($Env:GITHUB_WORKSPACE)\$($Env:SOLUTION_FILE)-$($Env:VERSION).zip" -Force -Write-Host "Output Contents post Zip" -Get-ChildItem -Path $destination \ No newline at end of file From 5c359db76cb134b3bb6dc2c2f011ca04fb2e5817 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 09:17:39 -0500 Subject: [PATCH 3/9] chore: install git hooks when the folder opens in VS Code --- .vscode/tasks.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .vscode/tasks.json diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..0b04565 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,18 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Install git hooks", + "type": "shell", + "command": "dotnet tool restore && dotnet husky install", + "presentation": { + "reveal": "silent", + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + }, + "problemMatcher": [] + } + ] +} From f97c558f67fb7dd893489ccc0f2a86ad04bb1dbb Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 09:21:50 -0500 Subject: [PATCH 4/9] docs: add Husky.Net setup and usage to README --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/README.md b/README.md index 1b1f719..a4573d5 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,48 @@ See the Task List in Visual Studio for a guide on how to start using the templat For renaming instructions in particular, see the XML `remarks` tags on class definitions +## Git Hooks (Husky.Net) + +This repo uses [Husky.Net](https://alirezanet.github.io/Husky.Net/) to run a `pre-commit` hook. The hook runs `.github/scripts/Update-ReadmeBadges.ps1`, which keeps the `.NET` and `PepperDash Essentials` badges at the top of this README in sync with the project: + +* `.NET` badge - `TargetFramework` in `src/*.4Series.csproj` +* `PepperDash Essentials` badge - the highest `MinimumEssentialsFrameworkVersion` in `src/*Factory.cs` + +If the badges change, `README.md` is re-staged automatically so the update is part of your commit. A warning is printed if the `PepperDashEssentials` package version in the csproj differs from `MinimumEssentialsFrameworkVersion`. + +### Requirements + +* [.NET SDK](https://dotnet.microsoft.com/download) (provides `dotnet tool`) +* Windows: the built-in Windows PowerShell 5.1 is used. macOS/Linux: [PowerShell 7 (`pwsh`)](https://learn.microsoft.com/powershell/scripting/install/installing-powershell) must be on the `PATH`. + +### Setup + +The hook is installed automatically the first time you do any of the following in a fresh clone: + +* Open the solution in Visual Studio, or run `dotnet restore` / `dotnet build` +* Open the folder in VS Code and allow the "Install git hooks" automatic task + +To install it manually: + +``` +dotnet tool restore +dotnet husky install +``` + +Verify with `git config core.hooksPath`, which should print `.husky`. + +### Usage + +* Commit as usual; the hook runs on every `git commit`. +* Run it on demand: `dotnet husky run` +* Check without modifying files (useful for CI): `pwsh .github/scripts/Update-ReadmeBadges.ps1 -Check` +* Skip the hook for a single commit: `git commit --no-verify` +* Skip the automatic install on restore: set the `HUSKY=0` environment variable. It is also skipped when `CI=true`. + +### Renaming + +The script locates files by pattern (`*.4Series.csproj`, `*Factory.cs`), so renaming the project or factory classes does not require changes. Keep a `MinimumEssentialsFrameworkVersion = "x.y.z";` assignment in at least one factory, and keep the two badges in this README. + ## Build Instructions (PepperDash Internal) ## Generating Nuget Package From 80a7ac61581beef37ea7f374964876d8f71fe0ac Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 10:53:08 -0500 Subject: [PATCH 5/9] chore: remove 3-Series leftovers, ignore code-workspace, update copyright year Delete GetPackages.BAT and the 3Series csproj, drop Peacock colors from VS Code settings, ignore *.code-workspace, and bump copyright to 2026. --- .gitignore | 3 + .vscode/settings.json | 22 +----- GetPackages.BAT | 2 - src/epi-make-model.3Series.csproj | 115 ------------------------------ src/epi-make-model.4Series.csproj | 2 +- 5 files changed, 5 insertions(+), 139 deletions(-) delete mode 100644 GetPackages.BAT delete mode 100644 src/epi-make-model.3Series.csproj diff --git a/.gitignore b/.gitignore index f218fa0..91861d4 100644 --- a/.gitignore +++ b/.gitignore @@ -357,3 +357,6 @@ MigrationBackup/ *.clz *.projectinfo *.suo + +# VS Code +*.code-workspace \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 1ac4b1c..e9f57a1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,24 +1,4 @@ { - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#8c57e5", - "activityBar.background": "#8c57e5", - "activityBar.foreground": "#e7e7e7", - "activityBar.inactiveForeground": "#e7e7e799", - "activityBarBadge.background": "#6e3411", - "activityBarBadge.foreground": "#e7e7e7", - "commandCenter.border": "#e7e7e799", - "sash.hoverBorder": "#8c57e5", - "statusBar.background": "#6e2bde", - "statusBar.foreground": "#e7e7e7", - "statusBarItem.hoverBackground": "#8c57e5", - "statusBarItem.remoteBackground": "#6e2bde", - "statusBarItem.remoteForeground": "#e7e7e7", - "titleBar.activeBackground": "#6e2bde", - "titleBar.activeForeground": "#e7e7e7", - "titleBar.inactiveBackground": "#6e2bde99", - "titleBar.inactiveForeground": "#e7e7e799" - }, - "peacock.color": "#6e2bde", "[csharp]": { "editor.defaultFormatter": "ms-dotnettools.csharp", "editor.formatOnSave": true, @@ -29,4 +9,4 @@ "csharp.inlayHints.enableInlayHintsForLambdaParameterTypes": true, "csharp.inlayHints.enableInlayHintsForTypes": true, "dotnet.formatting.organizeImportsOnFormat": true -} \ No newline at end of file +} diff --git a/GetPackages.BAT b/GetPackages.BAT deleted file mode 100644 index 9b6def3..0000000 --- a/GetPackages.BAT +++ /dev/null @@ -1,2 +0,0 @@ -@Echo ON -nuget install .\packages.config -OutputDirectory .\packages -excludeVersion diff --git a/src/epi-make-model.3Series.csproj b/src/epi-make-model.3Series.csproj deleted file mode 100644 index 03aa3ce..0000000 --- a/src/epi-make-model.3Series.csproj +++ /dev/null @@ -1,115 +0,0 @@ - - - Release - AnyCPU - 9.0.30729 - 2.0 - {9D249E47-8F95-4437-A6BB-563510287AD1} - Library - Properties - PepperDash.Essentials.Plugin - MakeModelPlugin - {0B4745B0-194B-4BB6-8E21-E9057CA92300};{4D628B5B-2FBC-4AA6-8C16-197242AEB884};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - WindowsCE - E2BECB1F-8C8C-41ba-B736-9BE7D946A398 - 5.0 - SmartDeviceProject1 - v3.5 - Windows CE - - - - - .allowedReferenceRelatedFileExtensions - true - full - false - bin\Debug\ - DEBUG;TRACE; - prompt - 4 - 512 - true - true - off - - - .allowedReferenceRelatedFileExtensions - none - true - bin\Release\ - prompt - 4 - 512 - true - true - off - - - - False - C:\ProgramData\Crestron\SDK\SSPDevices\Crestron.SimplSharpPro.UI.dll - - - False - ..\packages\PepperDashEssentials\lib\net35\Essentials Devices Common.dll - - - - False - ..\packages\PepperDashEssentials\lib\net35\PepperDashEssentials.dll - - - False - ..\packages\PepperDashEssentials\lib\net35\PepperDash_Core.dll - - - False - ..\packages\PepperDashEssentials\lib\net35\PepperDash_Essentials_Core.dll - - - False - ..\packages\PepperDashEssentials\lib\net35\PepperDash_Essentials_DM.dll - - - False - C:\ProgramData\Crestron\SDK\SimplSharpCustomAttributesInterface.dll - False - - - False - C:\ProgramData\Crestron\SDK\SimplSharpHelperInterface.dll - False - - - False - C:\ProgramData\Crestron\SDK\SimplSharpNewtonsoft.dll - - - False - C:\ProgramData\Crestron\SDK\SimplSharpPro.exe - False - - - False - C:\ProgramData\Crestron\SDK\SimplSharpReflectionInterface.dll - - - - - - - - - - - - - - - - - - rem S# Pro preparation will execute after these operations - - \ No newline at end of file diff --git a/src/epi-make-model.4Series.csproj b/src/epi-make-model.4Series.csproj index 081ac42..baaca9e 100644 --- a/src/epi-make-model.4Series.csproj +++ b/src/epi-make-model.4Series.csproj @@ -9,7 +9,7 @@ PepperDash.Essentials.Plugin.Make.Model PepperDash Technology This software is a template for a PepperDash Essentials Plugin. - Copyright 2025 + Copyright 2026 1.0.0-local true $(Version) From 9d3af8d44da98f044a21c614842a2f0aa019c77f Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 11:25:20 -0500 Subject: [PATCH 6/9] docs: address review feedback on badge hook - Update-ReadmeBadges: in -Stage mode fail when README.md or badge inputs have unstaged changes - README: document Windows PowerShell 5.1 check command and the unstaged-changes guard --- .github/scripts/Update-ReadmeBadges.ps1 | 10 ++++++++++ README.md | 5 ++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.github/scripts/Update-ReadmeBadges.ps1 b/.github/scripts/Update-ReadmeBadges.ps1 index e0f4240..1d7013a 100644 --- a/.github/scripts/Update-ReadmeBadges.ps1 +++ b/.github/scripts/Update-ReadmeBadges.ps1 @@ -23,6 +23,16 @@ function Fail($message) { exit 1 } +# Staging README.md adds its whole working-tree copy, and badges are computed from working-tree +# sources, so refuse to run when any of those inputs differ from the index. +if ($Stage) { + $unstaged = @(git -C $root diff --name-only -- README.md 'src/*.4Series.csproj' 'src/*Factory.cs') + if ($LASTEXITCODE) { Fail 'git diff failed' } + if ($unstaged.Count -gt 0) { + Fail ("unstaged changes in badge inputs ($($unstaged -join ', ')); stage or stash them, then commit again") + } +} + # Target framework from the 4Series csproj $csprojFile = Get-ChildItem -Path $srcDir -Filter '*.4Series.csproj' | Select-Object -First 1 if (-not $csprojFile) { Fail 'no *.4Series.csproj found in src/' } diff --git a/README.md b/README.md index a4573d5..571a683 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,10 @@ Verify with `git config core.hooksPath`, which should print `.husky`. * Commit as usual; the hook runs on every `git commit`. * Run it on demand: `dotnet husky run` -* Check without modifying files (useful for CI): `pwsh .github/scripts/Update-ReadmeBadges.ps1 -Check` +* Check without modifying files (useful for CI): + * Windows: `powershell -NoProfile -ExecutionPolicy Bypass -File .github/scripts/Update-ReadmeBadges.ps1 -Check` + * macOS/Linux: `pwsh -NoProfile -File .github/scripts/Update-ReadmeBadges.ps1 -Check` +* The hook refuses to run if `README.md`, the `.4Series.csproj`, or a `*Factory.cs` file has unstaged changes, so a partial commit never picks up unrelated edits. Stage or stash them and commit again. * Skip the hook for a single commit: `git commit --no-verify` * Skip the automatic install on restore: set the `HUSKY=0` environment variable. It is also skipped when `CI=true`. From e0f6fe55b1329e4e1094b3f0c57456b2301e575c Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 11:31:34 -0500 Subject: [PATCH 7/9] chore: use PowerShell 7 for README badge hook on all platforms --- .github/scripts/Update-ReadmeBadges.ps1 | 2 +- .husky/task-runner.json | 6 +----- README.md | 6 ++---- 3 files changed, 4 insertions(+), 10 deletions(-) diff --git a/.github/scripts/Update-ReadmeBadges.ps1 b/.github/scripts/Update-ReadmeBadges.ps1 index 1d7013a..347e8b9 100644 --- a/.github/scripts/Update-ReadmeBadges.ps1 +++ b/.github/scripts/Update-ReadmeBadges.ps1 @@ -13,7 +13,7 @@ param( ) $ErrorActionPreference = 'Stop' -$root = Resolve-Path (Join-Path $PSScriptRoot '..\..') +$root = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) $srcDir = Join-Path $root 'src' $readmePath = Join-Path $root 'README.md' $utf8 = New-Object System.Text.UTF8Encoding($false) diff --git a/.husky/task-runner.json b/.husky/task-runner.json index dc5f23f..34b5d9c 100644 --- a/.husky/task-runner.json +++ b/.husky/task-runner.json @@ -4,11 +4,7 @@ { "name": "update-readme-badges", "command": "pwsh", - "args": [ "-NoProfile", "-File", ".github/scripts/Update-ReadmeBadges.ps1", "-Stage" ], - "windows": { - "command": "powershell", - "args": [ "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", ".github/scripts/Update-ReadmeBadges.ps1", "-Stage" ] - } + "args": [ "-NoProfile", "-File", ".github/scripts/Update-ReadmeBadges.ps1", "-Stage" ] } ] } diff --git a/README.md b/README.md index 571a683..657aeb5 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ If the badges change, `README.md` is re-staged automatically so the update is pa ### Requirements * [.NET SDK](https://dotnet.microsoft.com/download) (provides `dotnet tool`) -* Windows: the built-in Windows PowerShell 5.1 is used. macOS/Linux: [PowerShell 7 (`pwsh`)](https://learn.microsoft.com/powershell/scripting/install/installing-powershell) must be on the `PATH`. +* [PowerShell 7+ (`pwsh`)](https://learn.microsoft.com/powershell/scripting/install/installing-powershell) on the `PATH` (Windows, macOS and Linux). Windows PowerShell 5.1 is not used. ### Setup @@ -76,9 +76,7 @@ Verify with `git config core.hooksPath`, which should print `.husky`. * Commit as usual; the hook runs on every `git commit`. * Run it on demand: `dotnet husky run` -* Check without modifying files (useful for CI): - * Windows: `powershell -NoProfile -ExecutionPolicy Bypass -File .github/scripts/Update-ReadmeBadges.ps1 -Check` - * macOS/Linux: `pwsh -NoProfile -File .github/scripts/Update-ReadmeBadges.ps1 -Check` +* Check without modifying files (useful for CI): `pwsh -NoProfile -File .github/scripts/Update-ReadmeBadges.ps1 -Check` * The hook refuses to run if `README.md`, the `.4Series.csproj`, or a `*Factory.cs` file has unstaged changes, so a partial commit never picks up unrelated edits. Stage or stash them and commit again. * Skip the hook for a single commit: `git commit --no-verify` * Skip the automatic install on restore: set the `HUSKY=0` environment variable. It is also skipped when `CI=true`. From c0fec3bf88b4d77de3a0cfeaf19cc3b2d8d5b25a Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 11:39:45 -0500 Subject: [PATCH 8/9] fix: harden badge hook staging guard and pin task shell to pwsh - Update-ReadmeBadges: include untracked badge inputs in the -Stage guard - Update-ReadmeBadges: fail the hook when git add fails - tasks.json: run the install task under pwsh so && works on Windows - README: mention untracked files in the guard note --- .github/scripts/Update-ReadmeBadges.ps1 | 13 +++++++++---- .vscode/tasks.json | 6 ++++++ README.md | 2 +- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.github/scripts/Update-ReadmeBadges.ps1 b/.github/scripts/Update-ReadmeBadges.ps1 index 347e8b9..72023a1 100644 --- a/.github/scripts/Update-ReadmeBadges.ps1 +++ b/.github/scripts/Update-ReadmeBadges.ps1 @@ -26,10 +26,12 @@ function Fail($message) { # Staging README.md adds its whole working-tree copy, and badges are computed from working-tree # sources, so refuse to run when any of those inputs differ from the index. if ($Stage) { - $unstaged = @(git -C $root diff --name-only -- README.md 'src/*.4Series.csproj' 'src/*Factory.cs') - if ($LASTEXITCODE) { Fail 'git diff failed' } + # Porcelain XY: a non-blank Y is a worktree change ('??' = untracked), which the index lacks. + $status = @(git -C $root status --porcelain --untracked-files=all -- README.md 'src/*.4Series.csproj' 'src/*Factory.cs') + if ($LASTEXITCODE) { Fail 'git status failed' } + $unstaged = @($status | Where-Object { $_ -match '^.[^ ]' }) if ($unstaged.Count -gt 0) { - Fail ("unstaged changes in badge inputs ($($unstaged -join ', ')); stage or stash them, then commit again") + Fail ("unstaged or untracked badge inputs ($(($unstaged | ForEach-Object { $_.Substring(3) }) -join ', ')); stage or stash them, then commit again") } } @@ -106,4 +108,7 @@ if ($Check) { Fail 'README.md badges are stale; run .github/scripts/Update-Readm [System.IO.File]::WriteAllText($readmePath, $updated, $utf8) Write-Host "badges: README.md updated ($($fw.Label) $($fw.Message), Essentials >= v$minVersion)" -if ($Stage) { git -C $root add README.md } +if ($Stage) { + git -C $root add README.md + if ($LASTEXITCODE) { Fail 'git add README.md failed' } +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0b04565..49c88ed 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,6 +5,12 @@ "label": "Install git hooks", "type": "shell", "command": "dotnet tool restore && dotnet husky install", + "options": { + "shell": { + "executable": "pwsh", + "args": ["-NoProfile", "-Command"] + } + }, "presentation": { "reveal": "silent", "close": true diff --git a/README.md b/README.md index 657aeb5..d127c04 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Verify with `git config core.hooksPath`, which should print `.husky`. * Commit as usual; the hook runs on every `git commit`. * Run it on demand: `dotnet husky run` * Check without modifying files (useful for CI): `pwsh -NoProfile -File .github/scripts/Update-ReadmeBadges.ps1 -Check` -* The hook refuses to run if `README.md`, the `.4Series.csproj`, or a `*Factory.cs` file has unstaged changes, so a partial commit never picks up unrelated edits. Stage or stash them and commit again. +* The hook refuses to run if `README.md`, the `.4Series.csproj`, or a `*Factory.cs` file has unstaged or untracked changes, so a partial commit never picks up unrelated edits. Stage or stash them and commit again. * Skip the hook for a single commit: `git commit --no-verify` * Skip the automatic install on restore: set the `HUSKY=0` environment variable. It is also skipped when `CI=true`. From a2d25975b4b9846dcb70f0841c2ba9a0a3d78147 Mon Sep 17 00:00:00 2001 From: jkdevito Date: Wed, 30 Sep 2026 11:43:18 -0500 Subject: [PATCH 9/9] chore: move dotnet tool manifest to .config --- dotnet-tools.json => .config/dotnet-tools.json | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dotnet-tools.json => .config/dotnet-tools.json (100%) diff --git a/dotnet-tools.json b/.config/dotnet-tools.json similarity index 100% rename from dotnet-tools.json rename to .config/dotnet-tools.json