diff --git a/.github/scripts/Test-ReadmeBadges.ps1 b/.github/scripts/Test-ReadmeBadges.ps1 new file mode 100644 index 0000000..67fd8fc --- /dev/null +++ b/.github/scripts/Test-ReadmeBadges.ps1 @@ -0,0 +1,109 @@ +[CmdletBinding()] +param() + +$ErrorActionPreference = 'Stop' +$fixture = Join-Path ([System.IO.Path]::GetTempPath()) ("readme-badges-" + [guid]::NewGuid()) +$utf8 = [System.Text.UTF8Encoding]::new($false) +$pwsh = (Get-Process -Id $PID).Path + +function Assert($condition, $message) { + if (-not $condition) { throw $message } +} + +function Write-Fixture($moniker, $packageVersion = '3.0.0-rc.19') { + [System.IO.File]::WriteAllText((Join-Path $fixture 'src/Example.4Series.csproj'), @" +$moniker + +"@, $utf8) + [System.IO.File]::WriteAllText((Join-Path $fixture 'src/ExampleFactory.cs'), @' +MinimumEssentialsFrameworkVersion = "2.12.1"; +MinimumEssentialsFrameworkVersion = "3.0.0"; +MinimumEssentialsFrameworkVersion = "2.9.0"; +'@, $utf8) + [System.IO.File]::WriteAllText($readme, $original, $utf8) +} + +function Invoke-Badges([string[]]$scriptArguments = @(), [int]$expectedExit = 0) { + # Run in a child process: the production script calls exit, including on success. + $output = & $pwsh -NoProfile -File $script @scriptArguments 2>&1 | Out-String + Assert ($LASTEXITCODE -eq $expectedExit) "Expected exit $expectedExit, got ${LASTEXITCODE}: $output" + return $output +} + +try { + New-Item -ItemType Directory -Path (Join-Path $fixture '.github/scripts'), (Join-Path $fixture 'src') -Force | Out-Null + $script = Join-Path $fixture '.github/scripts/Update-ReadmeBadges.ps1' + Copy-Item (Join-Path $PSScriptRoot 'Update-ReadmeBadges.ps1') $script + $readme = Join-Path $fixture 'README.md' + $original = @' +Unrelated text: ≥ keep this intact. +![PepperDash Essentials](https://img.shields.io/badge/stale-blue) +![.NET](https://img.shields.io/badge/stale-purple) +'@ + + $cases = @( + @('net472', '.NET%20Framework', '4.7.2'), + @('net48', '.NET%20Framework', '4.8'), + @('net10', '.NET%20Framework', '1.0'), # Existing legacy precedence is intentional. + @('net8', '.NET', '8'), + @('net8.0', '.NET', '8.0'), + @('net10.0', '.NET', '10.0'), + @('netstandard2.0', '.NET', 'netstandard2.0'), + @('net8.0junk', '.NET', 'net8.0junk'), + @('net8.0.1', '.NET', 'net8.0.1'), + @('net8.', '.NET', 'net8.'), + @('net8-windows', '.NET', 'net8--windows'), + @('net8.0_windows', '.NET', 'net8.0__windows'), + @('net', '.NET', 'net') + ) + foreach ($case in $cases) { + Write-Fixture $case[0] + $output = Invoke-Badges + Assert ($output -match 'package 3\.0\.0-rc\.19 != MinimumEssentialsFrameworkVersion 3\.0\.0') 'Literal package/minimum warning must be retained' + $updated = [System.IO.File]::ReadAllText($readme, $utf8) + $expected = $original.Replace('https://img.shields.io/badge/stale-purple', + "https://img.shields.io/badge/$($case[1])-$($case[2])-512BD4").Replace( + 'https://img.shields.io/badge/stale-blue', 'https://img.shields.io/badge/PepperDash%20Essentials-≥%20v3.0.0-blue') + Assert ($updated -ceq $expected) "Incorrect badges or unrelated README edits for $($case[0])" + Invoke-Badges @('-Check') | Out-Null + Invoke-Badges | Out-Null + Assert ([System.IO.File]::ReadAllText($readme, $utf8) -ceq $updated) "Update/-Check not idempotent for $($case[0])" + } + + Write-Fixture 'net8' '3.0.0' + $output = Invoke-Badges @('-Check') 1 + Assert ($output -match 'badges are stale') 'Expected stale badge failure' + Assert ($output -notmatch 'WARNING:') 'Matching numeric versions should not warn' + Assert ([System.IO.File]::ReadAllText($readme, $utf8) -ceq $original) '-Check must not write README' + + Write-Fixture 'net8' '3.1.0' + $output = Invoke-Badges + Assert ($output -match 'package 3\.1\.0 != MinimumEssentialsFrameworkVersion 3\.0\.0') 'Genuine mismatch warning was lost' + + & git -C $fixture init --quiet + Assert ($LASTEXITCODE -eq 0) 'Fixture git init failed' + & git -C $fixture add . + Assert ($LASTEXITCODE -eq 0) 'Fixture git add failed' + foreach ($inputPath in @('README.md', 'src/Example.4Series.csproj', 'src/ExampleFactory.cs', 'src/UntrackedFactory.cs')) { + $path = Join-Path $fixture $inputPath + [System.IO.File]::AppendAllText($path, "`nUnstaged input", $utf8) + $output = Invoke-Badges @('-Stage') 1 + Assert ($output -match 'unstaged or untracked badge inputs') "Stage guard failed for $inputPath" + if ($inputPath -eq 'src/UntrackedFactory.cs') { + Remove-Item $path + } else { + & git -C $fixture restore --worktree -- $inputPath + Assert ($LASTEXITCODE -eq 0) 'Fixture git restore failed' + } + } + Write-Fixture 'net8' + & git -C $fixture add . + Assert ($LASTEXITCODE -eq 0) 'Fixture git add failed' + Invoke-Badges @('-Stage') | Out-Null + $staged = & git -C $fixture show ':README.md' | Out-String + Assert ($LASTEXITCODE -eq 0 -and $staged.Contains('/badge/.NET-8-512BD4')) '-Stage did not stage the updated badge' + Invoke-Badges @('-Check') | Out-Null + Write-Host "Passed $($cases.Count) framework cases, update/-Check idempotence, warnings, and staging guards." +} finally { + if (Test-Path $fixture) { Remove-Item $fixture -Recurse -Force } +} diff --git a/.github/scripts/Update-ReadmeBadges.ps1 b/.github/scripts/Update-ReadmeBadges.ps1 index 72023a1..3bda989 100644 --- a/.github/scripts/Update-ReadmeBadges.ps1 +++ b/.github/scripts/Update-ReadmeBadges.ps1 @@ -66,7 +66,7 @@ function Get-FrameworkBadge($moniker) { if ($legacy.Groups[3].Success) { $parts += $legacy.Groups[3].Value } return @{ Label = '.NET Framework'; Message = ($parts -join '.') } } - $modern = [regex]::Match($moniker, '^net(\d+\.\d+)') + $modern = [regex]::Match($moniker, '^net(\d+(?:\.\d+)?)$') if ($modern.Success) { return @{ Label = '.NET'; Message = $modern.Groups[1].Value } } return @{ Label = '.NET'; Message = $moniker } } diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..52edf36 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,72 @@ +name: Validate plugin + +on: + pull_request: + paths: + - 'src/**' + - '*.4Series.sln' + - 'global.json' + - 'README.md' + - '.github/scripts/**' + - '.github/workflows/validate.yml' + push: + paths: + - 'src/**' + - '*.4Series.sln' + - 'global.json' + - 'README.md' + - '.github/scripts/**' + - '.github/workflows/validate.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + validate: + runs-on: windows-latest + strategy: + matrix: + configuration: [Debug, Release] + env: + HUSKY: '0' + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + - name: Badge regression tests + shell: pwsh + run: pwsh -NoProfile -File .github/scripts/Test-ReadmeBadges.ps1 + - name: Check README badges + shell: pwsh + run: pwsh -NoProfile -File .github/scripts/Update-ReadmeBadges.ps1 -Check + - name: Restore + run: dotnet restore epi-make-model.4Series.sln + - name: Build + run: dotnet build epi-make-model.4Series.sln --no-restore --configuration ${{ matrix.configuration }} + - name: Pack + run: dotnet pack src/epi-make-model.4Series.csproj --no-build --configuration ${{ matrix.configuration }} + - name: Check package contents + shell: pwsh + run: | + $ErrorActionPreference = 'Stop' + $cplz = Get-Item output/*.cplz + $package = Get-Item output/*.nupkg + $plugin = [System.IO.Compression.ZipFile]::OpenRead($cplz.FullName) + $nuget = [System.IO.Compression.ZipFile]::OpenRead($package.FullName) + try { + $dlls = @($plugin.Entries | Where-Object { $_.FullName -like '*.dll' }) + if ($dlls.Count -ne 1 -or $dlls[0].FullName -ne 'epi-make-model.4Series.dll') { + throw 'CPLZ must contain only the plugin DLL, not host-provided binaries' + } + if (-not $nuget.GetEntry('lib/net8.0/epi-make-model.4Series.dll') -or + -not $nuget.GetEntry("content/$($cplz.Name)")) { + throw 'NuGet package must contain the .NET 8 plugin DLL and CPLZ' + } + } finally { + $plugin.Dispose() + $nuget.Dispose() + } diff --git a/README.md b/README.md index d127c04..d78f93d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ ![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) +![PepperDash Essentials](https://img.shields.io/badge/PepperDash%20Essentials-≥%20v3.0.0-blue) +![.NET](https://img.shields.io/badge/.NET-8-512BD4) ![Crestron](https://img.shields.io/badge/Crestron-4--Series-lightgrey) ![License](https://img.shields.io/badge/license-MIT-green) @@ -30,11 +30,38 @@ After forking this repository into your own GitHub space, you can create a new r ## Dependencies -The [Essentials](https://github.com/PepperDash/Essentials) libraries are required. They referenced via nuget. You must have nuget.exe installed and in the `PATH` environment variable to use the following command. Nuget.exe is available at [nuget.org](https://dist.nuget.org/win-x86-commandline/latest/nuget.exe). +* Install the [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0) and PowerShell 7+ (`pwsh`). `global.json` selects an installed stable .NET 8 SDK, rolling forward to its latest installed feature band/patch. +* The [Essentials](https://github.com/PepperDash/Essentials) dependency is pinned directly in `src/epi-make-model.4Series.csproj` to **3.0.0-rc.19**, a prerelease, available from [nuget.org](https://www.nuget.org/packages/PepperDashEssentials/3.0.0-rc.19). No authenticated feed is required. +* The project uses literal `net8`, matching the [release-tagged Essentials v3 project](https://github.com/PepperDash/Essentials/blob/v3.0.0-rc.19/src/PepperDash.Essentials/PepperDash.Essentials.csproj). The .NET 8 SDK resolves this to .NET 8; NuGet uses the canonical `net8.0` asset folder. Keep the `.4Series` solution/project names for existing automation. +* Deployment requires a .NET 8-capable Crestron 4-Series/VC-4 environment running the matching Essentials v3 runtime. A successful build does not verify firmware compatibility or plugin loading on hardware. ### Installing Dependencies -Dependencies will be automatically installed when +Run from the repository root; `dotnet restore` installs the pinned dependencies: + +```powershell +dotnet restore epi-make-model.4Series.sln +dotnet build epi-make-model.4Series.sln --no-restore --configuration Debug +dotnet pack src/epi-make-model.4Series.csproj --no-build --configuration Debug +dotnet build epi-make-model.4Series.sln --no-restore --configuration Release +dotnet pack src/epi-make-model.4Series.csproj --no-build --configuration Release +pwsh -NoProfile -File .github/scripts/Test-ReadmeBadges.ps1 +pwsh -NoProfile -File .github/scripts/Update-ReadmeBadges.ps1 -Check +``` + +Builds generate a CPLZ and NuGet package in `output/`. The CPLZ includes the plugin output, not host-provided Essentials/Crestron dependencies (`ExcludeAssets=runtime`). Inspect both archives before deployment. `SERIES4` is defined for both configurations. + +The local `Validate plugin` workflow runs badge tests/checks and restore/build/pack in Debug and Release with the .NET 8 SDK, without publishing or requiring a semantic-release version. Existing release automation remains separate. + +### RC-to-GA follow-up + +All three factories use the numeric runtime minimum `3.0.0`, not a prerelease string. The [v3 runtime gate](https://github.com/PepperDash/Essentials/blob/v3.0.0-rc.19/src/PepperDash.Essentials.Core/Global/Global.cs#L233-L270) strips the host version to major/minor/build and parses the factory minimum as `System.Version`: RC19 therefore compares as `3.0.0`. This excludes v2 but cannot distinguish individual v3 RCs. The exact build dependency remains pinned to `3.0.0-rc.19`; verify deployment against that runtime rather than assuming every RC is compatible. + +This is an initial prerelease migration, not a promise of automatic updates: + +* [ ] When upstream CI publishes the official Essentials v3 NuGet package, verify its availability on nuget.org, then replace the pinned `3.0.0-rc.19` dependency with `3.0.0` (or a verified newer official v3 release). +* [ ] Restore, build both configurations, run badge regression tests and `-Check`, and pack using the commands above. Regenerate badges if the numeric runtime minimum changes. +* [ ] Inspect CPLZ/NuGet contents for accidental host dependencies and verify plugin loading, all three example device categories, communications, and bridge feedback on the intended Essentials runtime/hardware before deployment. ### Instructions for Renaming Solution and Files @@ -49,7 +76,7 @@ This repo uses [Husky.Net](https://alirezanet.github.io/Husky.Net/) to run a `pr * `.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`. +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`. With this RC migration, the literal `3.0.0-rc.19` package versus numeric `3.0.0` runtime minimum intentionally triggers that warning; it does not make `-Check` fail when badges are current. ### Requirements diff --git a/global.json b/global.json new file mode 100644 index 0000000..4c4c3ae --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.100", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} diff --git a/src/MakeModelCrestronDeviceFactory.cs b/src/MakeModelCrestronDeviceFactory.cs index f014b95..7e0d6a0 100644 --- a/src/MakeModelCrestronDeviceFactory.cs +++ b/src/MakeModelCrestronDeviceFactory.cs @@ -26,7 +26,7 @@ public class MakeModelCrestronDeviceFactory : EssentialsPluginDeviceFactory /// Set the minimum Essentials Framework Version /// - /// MinimumEssentialsFrameworkVersion = "1.6.4; + /// MinimumEssentialsFrameworkVersion = "3.0.0"; /// /// In the constructor we initialize the list with the typenames that will build an instance of this device /// @@ -37,7 +37,7 @@ public MakeModelCrestronDeviceFactory() { // Set the minimum Essentials Framework Version // TODO [ ] Update the Essentials minimum framework version which this plugin has been tested against - MinimumEssentialsFrameworkVersion = "2.12.1"; + MinimumEssentialsFrameworkVersion = "3.0.0"; // In the constructor we initialize the list with the typenames that will build an instance of this device // TODO [ ] Update the TypeNames for the plugin being developed @@ -84,4 +84,3 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D } } - diff --git a/src/MakeModelDeviceFactory.cs b/src/MakeModelDeviceFactory.cs index b767a29..3679915 100644 --- a/src/MakeModelDeviceFactory.cs +++ b/src/MakeModelDeviceFactory.cs @@ -24,7 +24,7 @@ public class MakeModelDeviceFactory : EssentialsPluginDeviceFactory /// Set the minimum Essentials Framework Version /// - /// MinimumEssentialsFrameworkVersion = "2.12.1; + /// MinimumEssentialsFrameworkVersion = "3.0.0"; /// /// In the constructor we initialize the list with the typenames that will build an instance of this device /// @@ -35,7 +35,7 @@ public MakeModelDeviceFactory() { // Set the minimum Essentials Framework Version // TODO [ ] Update the Essentials minimum framework version which this plugin has been tested against - MinimumEssentialsFrameworkVersion = "2.12.1"; + MinimumEssentialsFrameworkVersion = "3.0.0"; // In the constructor we initialize the list with the typenames that will build an instance of this device // TODO [ ] Update the TypeNames for the plugin being developed @@ -79,7 +79,7 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D } + } } - diff --git a/src/MakeModelLogicDeviceFactory.cs b/src/MakeModelLogicDeviceFactory.cs index 1f8cfa2..305fde8 100644 --- a/src/MakeModelLogicDeviceFactory.cs +++ b/src/MakeModelLogicDeviceFactory.cs @@ -24,7 +24,7 @@ public class MakeModelLogicDeviceFactory : EssentialsPluginDeviceFactory /// Set the minimum Essentials Framework Version /// - /// MinimumEssentialsFrameworkVersion = "1.6.4; + /// MinimumEssentialsFrameworkVersion = "3.0.0"; /// /// In the constructor we initialize the list with the typenames that will build an instance of this device /// @@ -35,7 +35,7 @@ public MakeModelLogicDeviceFactory() { // Set the minimum Essentials Framework Version // TODO [ ] Update the Essentials minimum framework version which this plugin has been tested against - MinimumEssentialsFrameworkVersion = "2.12.1"; + MinimumEssentialsFrameworkVersion = "3.0.0"; // In the constructor we initialize the list with the typenames that will build an instance of this device // TODO [ ] Update the TypeNames for the plugin being developed @@ -80,4 +80,3 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D } } - diff --git a/src/epi-make-model.4Series.csproj b/src/epi-make-model.4Series.csproj index baaca9e..314f9f1 100644 --- a/src/epi-make-model.4Series.csproj +++ b/src/epi-make-model.4Series.csproj @@ -3,7 +3,8 @@ ProgramLibrary - net472 + net8 + $(DefineConstants);SERIES4 PepperDash.Essentials.Plugin false PepperDash.Essentials.Plugin.Make.Model @@ -20,10 +21,6 @@ crestron 4series essentials plugin template - - $(DefineConstants);SERIES4 - - @@ -35,7 +32,7 @@ - + runtime