From 20efaa9391eb5572d0646459ad2d6357eb6c55c0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Oct 2026 18:16:08 +0000
Subject: [PATCH 1/2] Initial plan
From b66abb0694a79ad49f62ba2183658b342763c5f1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 27 Oct 2026 18:23:43 +0000
Subject: [PATCH 2/2] feat: migrate template to .NET 8 and Essentials v3 RC19
Co-authored-by: jonnyarndt <21110580+jonnyarndt@users.noreply.github.com>
---
.github/scripts/Test-ReadmeBadges.ps1 | 109 ++++++++++++++++++++++++
.github/scripts/Update-ReadmeBadges.ps1 | 2 +-
.github/workflows/validate.yml | 72 ++++++++++++++++
README.md | 37 ++++++--
global.json | 7 ++
src/MakeModelCrestronDeviceFactory.cs | 5 +-
src/MakeModelDeviceFactory.cs | 6 +-
src/MakeModelLogicDeviceFactory.cs | 5 +-
src/epi-make-model.4Series.csproj | 9 +-
9 files changed, 231 insertions(+), 21 deletions(-)
create mode 100644 .github/scripts/Test-ReadmeBadges.ps1
create mode 100644 .github/workflows/validate.yml
create mode 100644 global.json
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.
+
+
+'@
+
+ $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 @@

-
-
+
+


@@ -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