Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 109 additions & 0 deletions .github/scripts/Test-ReadmeBadges.ps1
Original file line number Diff line number Diff line change
@@ -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'), @"
<Project><PropertyGroup><TargetFramework>$moniker</TargetFramework></PropertyGroup>
<ItemGroup><PackageReference Include="PepperDashEssentials" Version="$packageVersion" /></ItemGroup></Project>
"@, $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 }
}
2 changes: 1 addition & 1 deletion .github/scripts/Update-ReadmeBadges.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}
Expand Down
72 changes: 72 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
@@ -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()
}
37 changes: 32 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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

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

Expand Down
7 changes: 7 additions & 0 deletions global.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"sdk": {
"version": "8.0.100",
"rollForward": "latestFeature",
"allowPrerelease": false
}
}
5 changes: 2 additions & 3 deletions src/MakeModelCrestronDeviceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class MakeModelCrestronDeviceFactory : EssentialsPluginDeviceFactory<Make
/// <example>
/// Set the minimum Essentials Framework Version
/// <code>
/// MinimumEssentialsFrameworkVersion = "1.6.4;
/// MinimumEssentialsFrameworkVersion = "3.0.0";
/// </code>
/// In the constructor we initialize the list with the typenames that will build an instance of this device
/// <code>
Expand All @@ -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
Expand Down Expand Up @@ -84,4 +84,3 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D
}

}

6 changes: 3 additions & 3 deletions src/MakeModelDeviceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class MakeModelDeviceFactory : EssentialsPluginDeviceFactory<MakeModelDev
/// <example>
/// Set the minimum Essentials Framework Version
/// <code>
/// MinimumEssentialsFrameworkVersion = "2.12.1;
/// MinimumEssentialsFrameworkVersion = "3.0.0";
/// </code>
/// In the constructor we initialize the list with the typenames that will build an instance of this device
/// <code>
Expand All @@ -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
Expand Down Expand Up @@ -79,7 +79,7 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D

}


}

}

5 changes: 2 additions & 3 deletions src/MakeModelLogicDeviceFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class MakeModelLogicDeviceFactory : EssentialsPluginDeviceFactory<MakeMod
/// <example>
/// Set the minimum Essentials Framework Version
/// <code>
/// MinimumEssentialsFrameworkVersion = "1.6.4;
/// MinimumEssentialsFrameworkVersion = "3.0.0";
/// </code>
/// In the constructor we initialize the list with the typenames that will build an instance of this device
/// <code>
Expand All @@ -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
Expand Down Expand Up @@ -80,4 +80,3 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D
}

}

9 changes: 3 additions & 6 deletions src/epi-make-model.4Series.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<ProjectType>ProgramLibrary</ProjectType>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
<TargetFramework>net8</TargetFramework>
<DefineConstants>$(DefineConstants);SERIES4</DefineConstants>
<RootNamespace>PepperDash.Essentials.Plugin</RootNamespace>
<Deterministic>false</Deterministic>
<AssemblyTitle>PepperDash.Essentials.Plugin.Make.Model</AssemblyTitle>
Expand All @@ -20,10 +21,6 @@
<PackageTags>crestron 4series essentials plugin template</PackageTags>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>$(DefineConstants);SERIES4</DefineConstants>
</PropertyGroup>

<ItemGroup>
<Compile Remove="Properties\**" />
<EmbeddedResource Remove="Properties\**" />
Expand All @@ -35,7 +32,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="PepperDashEssentials" Version="2.12.1" >
<PackageReference Include="PepperDashEssentials" Version="3.0.0-rc.19" >
<ExcludeAssets>runtime</ExcludeAssets>
</PackageReference>
</ItemGroup>
Expand Down