diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c41731d..158aedc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,51 +1,51 @@ -name: CI - -# Build + unit tests on every push and PR to master. Keeps the release path -# (release.yml) lean — this catches regressions continuously, not only at release. -on: - push: - branches: [master] - pull_request: - branches: [master] - workflow_dispatch: - -permissions: - contents: read - -concurrency: - group: ci-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-test: - runs-on: windows-latest - env: - # The ProjectReference to ZeroZero.Brand.WinUI defaults to the sibling ..\0z0-shared folder - # for local dev; CI only has this repo, so we check the 0z0-shared repo out below and point the - # csproj's $(ZeroZeroSharedDir) property at it via this env var (MSBuild reads env as a property). - ZeroZeroSharedDir: ${{ github.workspace }}/0z0-shared - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Checkout 0z0-shared (sibling dependency) - uses: actions/checkout@v4 - with: - repository: 0z00z0/0z0-shared - path: 0z0-shared - - - name: Set up .NET 10 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.x' - - # The app targets win-x64 and does ReadyToRun on Release, which needs the - # win-x64 runtime pack — so restore/build must specify the RID (NETSDK1112). - - name: Restore - run: dotnet restore HyperVManagerTray.csproj -r win-x64 - - - name: Build (Release) - run: dotnet build HyperVManagerTray.csproj -c Release -r win-x64 --no-restore --nologo - - - name: Unit tests - run: dotnet test Tests\HyperVManagerTray.Tests.csproj -c Release --nologo +name: CI + +# Build + unit tests on every push and PR to master. Keeps the release path +# (release.yml) lean — this catches regressions continuously, not only at release. +on: + push: + branches: [master] + pull_request: + branches: [master] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-test: + runs-on: windows-latest + env: + # The ProjectReference to ZeroZero.Brand.WinUI defaults to the sibling ..\0z0-shared folder + # for local dev; CI only has this repo, so we check the 0z0-shared repo out below and point the + # csproj's $(ZeroZeroSharedDir) property at it via this env var (MSBuild reads env as a property). + ZeroZeroSharedDir: ${{ github.workspace }}/0z0-shared + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Checkout 0z0-shared (sibling dependency) + uses: actions/checkout@v7 + with: + repository: 0z00z0/0z0-shared + path: 0z0-shared + + - name: Set up .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.x' + + # The app targets win-x64 and does ReadyToRun on Release, which needs the + # win-x64 runtime pack — so restore/build must specify the RID (NETSDK1112). + - name: Restore + run: dotnet restore HyperVManagerTray.csproj -r win-x64 + + - name: Build (Release) + run: dotnet build HyperVManagerTray.csproj -c Release -r win-x64 --no-restore --nologo + + - name: Unit tests + run: dotnet test Tests\HyperVManagerTray.Tests.csproj -c Release --nologo diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5620e40..75ee2ed 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,282 +1,282 @@ -name: Release - -on: - push: - tags: - - 'v*.*.*' - workflow_dispatch: - inputs: - version: - description: 'Version to release (e.g. 2.1.8 — without the leading v)' - required: false - default: '' - -permissions: - contents: write - -jobs: - # ───────────────────────────────────────────────────────────────────────────── - # Build, sign, and publish the installer - # ───────────────────────────────────────────────────────────────────────────── - release: - runs-on: windows-latest - - # Secrets surfaced as job-level env so step `if:` conditions can test them - # (a step's own `env:` block is NOT visible to that step's `if:`). - env: - CODE_SIGN_PFX: ${{ secrets.CODE_SIGN_PFX }} - CODE_SIGN_PASSWORD: ${{ secrets.CODE_SIGN_PASSWORD }} - # ProjectReference to ZeroZero.Brand.WinUI defaults to the sibling ..\0z0-shared folder for - # local dev; CI only has this repo, so we check the 0z0-shared repo out below and point the - # csproj's $(ZeroZeroSharedDir) property at it via this env var (MSBuild reads env as a property). - ZeroZeroSharedDir: ${{ github.workspace }}/0z0-shared - - outputs: - version: ${{ steps.version.outputs.version }} - - steps: - # ── 1. Checkout (full history so tags resolve) ────────────────────────── - - name: Checkout - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - # Check out the 0z0-shared sibling so the shared-About ProjectReference resolves (see the - # ZeroZeroSharedDir env var above). Placed in a workspace subfolder — not ..\ (outside workspace). - - name: Checkout 0z0-shared (sibling dependency) - uses: actions/checkout@v4 - with: - repository: 0z00z0/0z0-shared - path: 0z0-shared - - # ── 2. Resolve version string ──────────────────────────────────────────── - # Tag-triggered: strip the leading 'v'. - # workflow_dispatch: use the input; fall back to '0.0.0-dev'. - - name: Resolve version - id: version - shell: pwsh - run: | - if ($env:GITHUB_REF -match '^refs/tags/v(.+)$') { - $ver = $Matches[1] - } elseif ('${{ github.event.inputs.version }}' -ne '') { - $ver = '${{ github.event.inputs.version }}' - } else { - $ver = '0.0.0-dev' - } - Write-Host "Version: $ver" - "version=$ver" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - - # ── 3. Set up .NET 10 ──────────────────────────────────────────────────── - - name: Set up .NET 10 - uses: actions/setup-dotnet@v4 - with: - dotnet-version: '10.x' - - # ── 4. Run unit tests (gate the release) ───────────────────────────────── - # No installer is published unless all tests pass. - - name: Run unit tests - shell: pwsh - run: | - dotnet test Tests\HyperVManagerTray.Tests.csproj -c Release --nologo - if ($LASTEXITCODE -ne 0) { throw "Unit tests failed (exit $LASTEXITCODE)." } - - # ── 5. Install Inno Setup 6 via Chocolatey ─────────────────────────────── - - name: Install Inno Setup 6 - shell: pwsh - run: choco install innosetup -y --no-progress - - # ── 6. Build installer (publish + ISCC) ────────────────────────────────── - # build-installer.ps1 runs dotnet publish internally and compiles the .iss. - # Passing -Version pins the release version (no auto-bump in CI). - - name: Build installer - shell: pwsh - run: | - $ver = '${{ steps.version.outputs.version }}' - Write-Host "Building installer for version $ver" - & installer\build-installer.ps1 -Version $ver - - # ── 7. Authenticode sign (skipped gracefully when secrets are absent) ──── - # Signs with the project's certificate (PFX delivered via the CODE_SIGN_PFX - # secret — the same cert used for local builds). RFC-3161 timestamped so the - # signature stays valid after the certificate expires. The app exe is signed - # FIRST, then the installer is recompiled (so it bundles the signed exe) and - # signed once — the shipped installer never embeds an unsigned exe. - - name: Sign binaries - if: ${{ env.CODE_SIGN_PFX != '' }} - shell: pwsh - run: | - $pfxPath = Join-Path $env:RUNNER_TEMP 'codesign.pfx' - [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:CODE_SIGN_PFX)) - - $signtool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' ` - -Filter 'signtool.exe' -Recurse -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -like '*x64*' } | - Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName - if (-not $signtool) { throw 'signtool.exe not found under Windows Kits.' } - Write-Host "Using signtool: $signtool" - - function Invoke-Sign([string]$path) { - & $signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 ` - /f $pfxPath /p $env:CODE_SIGN_PASSWORD $path - if ($LASTEXITCODE -ne 0) { throw "signtool failed on $path ($LASTEXITCODE)." } - } - - # 1) Sign the published app exe. - Invoke-Sign 'publish\HyperVManagerTray.exe' - - # 2) Recompile the installer (now bundling the signed exe), then sign it. - $iscc = (Get-ChildItem 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' -ErrorAction SilentlyContinue).FullName - if (-not $iscc) { throw 'ISCC.exe not found.' } - & $iscc "/DAppVersion=${{ steps.version.outputs.version }}" "/DPublishDir=$(Resolve-Path publish)" installer\HyperVManagerTray.iss - if ($LASTEXITCODE -ne 0) { throw "ISCC failed ($LASTEXITCODE)." } - Invoke-Sign "installer\Output\HyperVManagerTray-Setup-${{ steps.version.outputs.version }}.exe" - - Remove-Item $pfxPath -Force - Write-Host 'Signed app exe + installer; PFX deleted.' - - # ── 7b. Warn if signing was skipped (no secret) ───────────────────────── - - name: Warn if unsigned - if: ${{ env.CODE_SIGN_PFX == '' }} - shell: pwsh - run: Write-Warning 'CODE_SIGN_PFX secret not set — installer is UNSIGNED. Add the secret to sign CI releases.' - - # ── 8. Compute SHA256 of the installer ─────────────────────────────────── - - name: Compute installer SHA256 - id: sha256 - shell: pwsh - run: | - $setup = "installer\Output\HyperVManagerTray-Setup-${{ steps.version.outputs.version }}.exe" - $hash = (Get-FileHash $setup -Algorithm SHA256).Hash.ToUpper() - Write-Host "SHA256: $hash" - "sha256=$hash" | Out-File -FilePath $env:GITHUB_OUTPUT -Append - - # ── 9. Update winget manifests ─────────────────────────────────────────── - - name: Update winget manifest - shell: pwsh - run: | - $ver = '${{ steps.version.outputs.version }}' - $sha256 = '${{ steps.sha256.outputs.sha256 }}' - $tag = "v$ver" - - $installerYaml = 'installer\winget\0z00z0.HyperVManagerTray.installer.yaml' - $versionYaml = 'installer\winget\0z00z0.HyperVManagerTray.yaml' - $localeYaml = 'installer\winget\0z00z0.HyperVManagerTray.locale.en-US.yaml' - - # Helper: replace a YAML scalar in-place - function Set-YamlValue { - param([string]$File, [string]$Key, [string]$Value) - $content = Get-Content $File -Raw - $content = $content -replace "(?m)^($Key\s*:\s*).*$", "`${1}$Value" - Set-Content $File -Value $content -NoNewline - } - - # installer manifest - Set-YamlValue $installerYaml 'PackageVersion' $ver - Set-YamlValue $installerYaml 'InstallerSha256' $sha256 - - # Update InstallerUrl to point at the new release asset - $url = "https://github.com/${{ github.repository }}/releases/download/$tag/HyperVManagerTray-Setup-$ver.exe" - Set-YamlValue $installerYaml 'InstallerUrl' $url - - # Also update DisplayVersion inside AppsAndFeaturesEntries - Set-YamlValue $installerYaml 'DisplayVersion' $ver - - # version manifest - Set-YamlValue $versionYaml 'PackageVersion' $ver - - # locale manifest - Set-YamlValue $localeYaml 'PackageVersion' $ver - - Write-Host "Manifests updated:" - Get-Content $installerYaml | Write-Host - - # ── 10. Validate winget manifests (against the patched files) ──────────── - - name: Validate winget manifests - shell: pwsh - run: | - $wg = Get-Command winget -ErrorAction SilentlyContinue - if (-not $wg) { - Write-Warning 'winget not found on this runner; skipping manifest validation.' - } else { - winget validate --manifest installer\winget - if ($LASTEXITCODE -ne 0) { - Write-Error "winget validate failed (exit $LASTEXITCODE)." - exit $LASTEXITCODE - } - Write-Host 'Manifests are valid.' - } - - # ── 11. Create GitHub Release (tag pushes only) ────────────────────────── - # A manual workflow_dispatch is a dry-run: it builds, tests, signs, and - # validates the manifests but does NOT publish a release. - - name: Create GitHub Release - if: ${{ github.ref_type == 'tag' }} - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}', steps.version.outputs.version) }} - name: Hyper-V Manager Tray v${{ steps.version.outputs.version }} - draft: false - prerelease: false - files: | - installer/Output/HyperVManagerTray-Setup-*.exe - installer/winget/0z00z0.HyperVManagerTray.installer.yaml - installer/winget/0z00z0.HyperVManagerTray.yaml - installer/winget/0z00z0.HyperVManagerTray.locale.en-US.yaml - scripts/ZeroZeroSoftware.cer - body: | - ## Hyper-V Manager Tray v${{ steps.version.outputs.version }} - - **Install / upgrade via winget:** - ``` - winget install 0z00z0.HyperVManagerTray - winget upgrade 0z00z0.HyperVManagerTray - ``` - - Or download `HyperVManagerTray-Setup-${{ steps.version.outputs.version }}.exe` below — per-user installer, no admin required. - - **SHA256 (installer):** `${{ steps.sha256.outputs.sha256 }}` - - # ───────────────────────────────────────────────────────────────────────────── - # Post-release: download the manifest assets from the release and re-validate - # ───────────────────────────────────────────────────────────────────────────── - validate-manifests: - runs-on: windows-latest - needs: release - # Only runs for real (tag) releases — a dispatch dry-run publishes no assets. - if: ${{ github.ref_type == 'tag' }} - - steps: - # Download the three winget manifest assets that were attached to the release - - name: Download winget manifests from release - shell: pwsh - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - $tag = 'v${{ needs.release.outputs.version }}' - $outDir = 'winget-validate' - New-Item -ItemType Directory -Force -Path $outDir | Out-Null - - $assets = @( - '0z00z0.HyperVManagerTray.installer.yaml', - '0z00z0.HyperVManagerTray.yaml', - '0z00z0.HyperVManagerTray.locale.en-US.yaml' - ) - foreach ($a in $assets) { - gh release download $tag --repo ${{ github.repository }} --pattern $a --dir $outDir - } - - - name: Validate downloaded manifests - shell: pwsh - run: | - $wg = Get-Command winget -ErrorAction SilentlyContinue - if (-not $wg) { - Write-Warning 'winget not found on this runner; skipping manifest validation.' - exit 0 - } - - winget validate --manifest winget-validate - if ($LASTEXITCODE -ne 0) { - Write-Error "winget validate failed (exit $LASTEXITCODE)." - exit $LASTEXITCODE - } - Write-Host 'Released manifests are valid.' +name: Release + +on: + push: + tags: + - 'v*.*.*' + workflow_dispatch: + inputs: + version: + description: 'Version to release (e.g. 2.1.8 — without the leading v)' + required: false + default: '' + +permissions: + contents: write + +jobs: + # ───────────────────────────────────────────────────────────────────────────── + # Build, sign, and publish the installer + # ───────────────────────────────────────────────────────────────────────────── + release: + runs-on: windows-latest + + # Secrets surfaced as job-level env so step `if:` conditions can test them + # (a step's own `env:` block is NOT visible to that step's `if:`). + env: + CODE_SIGN_PFX: ${{ secrets.CODE_SIGN_PFX }} + CODE_SIGN_PASSWORD: ${{ secrets.CODE_SIGN_PASSWORD }} + # ProjectReference to ZeroZero.Brand.WinUI defaults to the sibling ..\0z0-shared folder for + # local dev; CI only has this repo, so we check the 0z0-shared repo out below and point the + # csproj's $(ZeroZeroSharedDir) property at it via this env var (MSBuild reads env as a property). + ZeroZeroSharedDir: ${{ github.workspace }}/0z0-shared + + outputs: + version: ${{ steps.version.outputs.version }} + + steps: + # ── 1. Checkout (full history so tags resolve) ────────────────────────── + - name: Checkout + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + # Check out the 0z0-shared sibling so the shared-About ProjectReference resolves (see the + # ZeroZeroSharedDir env var above). Placed in a workspace subfolder — not ..\ (outside workspace). + - name: Checkout 0z0-shared (sibling dependency) + uses: actions/checkout@v7 + with: + repository: 0z00z0/0z0-shared + path: 0z0-shared + + # ── 2. Resolve version string ──────────────────────────────────────────── + # Tag-triggered: strip the leading 'v'. + # workflow_dispatch: use the input; fall back to '0.0.0-dev'. + - name: Resolve version + id: version + shell: pwsh + run: | + if ($env:GITHUB_REF -match '^refs/tags/v(.+)$') { + $ver = $Matches[1] + } elseif ('${{ github.event.inputs.version }}' -ne '') { + $ver = '${{ github.event.inputs.version }}' + } else { + $ver = '0.0.0-dev' + } + Write-Host "Version: $ver" + "version=$ver" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + + # ── 3. Set up .NET 10 ──────────────────────────────────────────────────── + - name: Set up .NET 10 + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '10.x' + + # ── 4. Run unit tests (gate the release) ───────────────────────────────── + # No installer is published unless all tests pass. + - name: Run unit tests + shell: pwsh + run: | + dotnet test Tests\HyperVManagerTray.Tests.csproj -c Release --nologo + if ($LASTEXITCODE -ne 0) { throw "Unit tests failed (exit $LASTEXITCODE)." } + + # ── 5. Install Inno Setup 6 via Chocolatey ─────────────────────────────── + - name: Install Inno Setup 6 + shell: pwsh + run: choco install innosetup -y --no-progress + + # ── 6. Build installer (publish + ISCC) ────────────────────────────────── + # build-installer.ps1 runs dotnet publish internally and compiles the .iss. + # Passing -Version pins the release version (no auto-bump in CI). + - name: Build installer + shell: pwsh + run: | + $ver = '${{ steps.version.outputs.version }}' + Write-Host "Building installer for version $ver" + & installer\build-installer.ps1 -Version $ver + + # ── 7. Authenticode sign (skipped gracefully when secrets are absent) ──── + # Signs with the project's certificate (PFX delivered via the CODE_SIGN_PFX + # secret — the same cert used for local builds). RFC-3161 timestamped so the + # signature stays valid after the certificate expires. The app exe is signed + # FIRST, then the installer is recompiled (so it bundles the signed exe) and + # signed once — the shipped installer never embeds an unsigned exe. + - name: Sign binaries + if: ${{ env.CODE_SIGN_PFX != '' }} + shell: pwsh + run: | + $pfxPath = Join-Path $env:RUNNER_TEMP 'codesign.pfx' + [IO.File]::WriteAllBytes($pfxPath, [Convert]::FromBase64String($env:CODE_SIGN_PFX)) + + $signtool = Get-ChildItem 'C:\Program Files (x86)\Windows Kits\10\bin' ` + -Filter 'signtool.exe' -Recurse -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like '*x64*' } | + Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName + if (-not $signtool) { throw 'signtool.exe not found under Windows Kits.' } + Write-Host "Using signtool: $signtool" + + function Invoke-Sign([string]$path) { + & $signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 ` + /f $pfxPath /p $env:CODE_SIGN_PASSWORD $path + if ($LASTEXITCODE -ne 0) { throw "signtool failed on $path ($LASTEXITCODE)." } + } + + # 1) Sign the published app exe. + Invoke-Sign 'publish\HyperVManagerTray.exe' + + # 2) Recompile the installer (now bundling the signed exe), then sign it. + $iscc = (Get-ChildItem 'C:\Program Files (x86)\Inno Setup 6\ISCC.exe' -ErrorAction SilentlyContinue).FullName + if (-not $iscc) { throw 'ISCC.exe not found.' } + & $iscc "/DAppVersion=${{ steps.version.outputs.version }}" "/DPublishDir=$(Resolve-Path publish)" installer\HyperVManagerTray.iss + if ($LASTEXITCODE -ne 0) { throw "ISCC failed ($LASTEXITCODE)." } + Invoke-Sign "installer\Output\HyperVManagerTray-Setup-${{ steps.version.outputs.version }}.exe" + + Remove-Item $pfxPath -Force + Write-Host 'Signed app exe + installer; PFX deleted.' + + # ── 7b. Warn if signing was skipped (no secret) ───────────────────────── + - name: Warn if unsigned + if: ${{ env.CODE_SIGN_PFX == '' }} + shell: pwsh + run: Write-Warning 'CODE_SIGN_PFX secret not set — installer is UNSIGNED. Add the secret to sign CI releases.' + + # ── 8. Compute SHA256 of the installer ─────────────────────────────────── + - name: Compute installer SHA256 + id: sha256 + shell: pwsh + run: | + $setup = "installer\Output\HyperVManagerTray-Setup-${{ steps.version.outputs.version }}.exe" + $hash = (Get-FileHash $setup -Algorithm SHA256).Hash.ToUpper() + Write-Host "SHA256: $hash" + "sha256=$hash" | Out-File -FilePath $env:GITHUB_OUTPUT -Append + + # ── 9. Update winget manifests ─────────────────────────────────────────── + - name: Update winget manifest + shell: pwsh + run: | + $ver = '${{ steps.version.outputs.version }}' + $sha256 = '${{ steps.sha256.outputs.sha256 }}' + $tag = "v$ver" + + $installerYaml = 'installer\winget\0z00z0.HyperVManagerTray.installer.yaml' + $versionYaml = 'installer\winget\0z00z0.HyperVManagerTray.yaml' + $localeYaml = 'installer\winget\0z00z0.HyperVManagerTray.locale.en-US.yaml' + + # Helper: replace a YAML scalar in-place + function Set-YamlValue { + param([string]$File, [string]$Key, [string]$Value) + $content = Get-Content $File -Raw + $content = $content -replace "(?m)^($Key\s*:\s*).*$", "`${1}$Value" + Set-Content $File -Value $content -NoNewline + } + + # installer manifest + Set-YamlValue $installerYaml 'PackageVersion' $ver + Set-YamlValue $installerYaml 'InstallerSha256' $sha256 + + # Update InstallerUrl to point at the new release asset + $url = "https://github.com/${{ github.repository }}/releases/download/$tag/HyperVManagerTray-Setup-$ver.exe" + Set-YamlValue $installerYaml 'InstallerUrl' $url + + # Also update DisplayVersion inside AppsAndFeaturesEntries + Set-YamlValue $installerYaml 'DisplayVersion' $ver + + # version manifest + Set-YamlValue $versionYaml 'PackageVersion' $ver + + # locale manifest + Set-YamlValue $localeYaml 'PackageVersion' $ver + + Write-Host "Manifests updated:" + Get-Content $installerYaml | Write-Host + + # ── 10. Validate winget manifests (against the patched files) ──────────── + - name: Validate winget manifests + shell: pwsh + run: | + $wg = Get-Command winget -ErrorAction SilentlyContinue + if (-not $wg) { + Write-Warning 'winget not found on this runner; skipping manifest validation.' + } else { + winget validate --manifest installer\winget + if ($LASTEXITCODE -ne 0) { + Write-Error "winget validate failed (exit $LASTEXITCODE)." + exit $LASTEXITCODE + } + Write-Host 'Manifests are valid.' + } + + # ── 11. Create GitHub Release (tag pushes only) ────────────────────────── + # A manual workflow_dispatch is a dry-run: it builds, tests, signs, and + # validates the manifests but does NOT publish a release. + - name: Create GitHub Release + if: ${{ github.ref_type == 'tag' }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_type == 'tag' && github.ref_name || format('v{0}', steps.version.outputs.version) }} + name: Hyper-V Manager Tray v${{ steps.version.outputs.version }} + draft: false + prerelease: false + files: | + installer/Output/HyperVManagerTray-Setup-*.exe + installer/winget/0z00z0.HyperVManagerTray.installer.yaml + installer/winget/0z00z0.HyperVManagerTray.yaml + installer/winget/0z00z0.HyperVManagerTray.locale.en-US.yaml + scripts/ZeroZeroSoftware.cer + body: | + ## Hyper-V Manager Tray v${{ steps.version.outputs.version }} + + **Install / upgrade via winget:** + ``` + winget install 0z00z0.HyperVManagerTray + winget upgrade 0z00z0.HyperVManagerTray + ``` + + Or download `HyperVManagerTray-Setup-${{ steps.version.outputs.version }}.exe` below — per-user installer, no admin required. + + **SHA256 (installer):** `${{ steps.sha256.outputs.sha256 }}` + + # ───────────────────────────────────────────────────────────────────────────── + # Post-release: download the manifest assets from the release and re-validate + # ───────────────────────────────────────────────────────────────────────────── + validate-manifests: + runs-on: windows-latest + needs: release + # Only runs for real (tag) releases — a dispatch dry-run publishes no assets. + if: ${{ github.ref_type == 'tag' }} + + steps: + # Download the three winget manifest assets that were attached to the release + - name: Download winget manifests from release + shell: pwsh + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + $tag = 'v${{ needs.release.outputs.version }}' + $outDir = 'winget-validate' + New-Item -ItemType Directory -Force -Path $outDir | Out-Null + + $assets = @( + '0z00z0.HyperVManagerTray.installer.yaml', + '0z00z0.HyperVManagerTray.yaml', + '0z00z0.HyperVManagerTray.locale.en-US.yaml' + ) + foreach ($a in $assets) { + gh release download $tag --repo ${{ github.repository }} --pattern $a --dir $outDir + } + + - name: Validate downloaded manifests + shell: pwsh + run: | + $wg = Get-Command winget -ErrorAction SilentlyContinue + if (-not $wg) { + Write-Warning 'winget not found on this runner; skipping manifest validation.' + exit 0 + } + + winget validate --manifest winget-validate + if ($LASTEXITCODE -ne 0) { + Write-Error "winget validate failed (exit $LASTEXITCODE)." + exit $LASTEXITCODE + } + Write-Host 'Released manifests are valid.'