Compare commits

...
2 Commits
Author SHA1 Message Date
koftwentytwo-standards-releases[bot] 0ef3d319b8 chore(main): release 1.2.0 (#48)
Release proposal generated from reviewed changes on main. Review the
version, changes, and validation before merging.
---


## 1.2.0 (2026-10-13)

<!-- Release notes generated using configuration in .github/release.yml
at main -->

## What's Changed
### Changes
* feat(releases): publish signed per-user MSI installers by
@KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/47


**Full Changelog**:
https://github.com/KofTwentyTwo/standards/compare/v1.1.2...v1.2.0

---
Generated by [Release
Please](https://github.com/googleapis/release-please) using this
repository's release configuration.

Co-authored-by: koftwentytwo-standards-releases[bot] <340613191+koftwentytwo-standards-releases[bot]@users.noreply.github.com>
2026-10-13 08:10:06 -05:00
james.maesandCodex GPT-6 7584fa19cc feat(releases): publish signed per-user MSI installers (#47)
Provide optional per-user MSI alongside existing EXE/ZIP assets using
pinned Velopack1.2.161. Keep the existing signing hook, preserve
third-party signatures, verify the administratively extracted MSI
payload through the existing signed-archive verifier, require native MSI
install/window launch/uninstall before publication, and include MSI in
SBOM subjects/checksums/provenance. The default remains EXE-only for
existing callers.

Validation: actionlint and configured zizmor medium gate pass; all40
signing regressions pass with zero skips. A real native MSI fixture
compiles and the actual GitHub-wrapped extraction body preserves all6
signed input application binaries byte-for-byte. Native MSI ProductCode
lookup succeeds. The first local administrative target exceeded Windows
Installer's path limit; a short normal temporary root succeeds, matching
hosted runner paths. This fixture is unsigned and not published;
production MSI signing/installation is required before claiming
delivery.

The maintainer explicitly renewed EXE+MSI delivery and instructed
completion without further human-review requests. Existing publication
authorization applies. No Azure identity, scope or credential changes.
Substantial AI assistance: Codex GPT-6. Refs
KofTwentyTwo/MarkdownViewr.Windows#36 and #33.

Signed-off-by: James Maes <james@kof22.com>
Co-authored-by: Codex GPT-6 <noreply@openai.com>
2026-10-13 08:05:38 -05:00
4 changed files with 129 additions and 3 deletions

No files matched your search

+116 -1
View File
@@ -108,6 +108,10 @@ on:
description: Newline-separated required owned file names or publish-relative patterns, including the optional CLI; required when sign is true.
type: string
default: ""
msi:
description: Generate a per-user MSI alongside Setup.exe and portable ZIP; requires the same signing and verification.
type: boolean
default: false
outputs:
version:
description: The released version (tag without the leading v).
@@ -494,6 +498,8 @@ jobs:
- name: Velopack pack
shell: pwsh
env:
VPK_MSI: ${{ inputs.msi }}
run: |
$sign = @()
if ($env:K22_SIGN -eq 'true')
@@ -505,7 +511,7 @@ jobs:
$sign = @('--signTemplate', $command, '--signExclude', $exclude)
}
$env:VELOPACK_TEMP = Join-Path $env:RUNNER_TEMP 'k22-velopack'
vpk pack --runtime $env:K22_RUNTIME --packId $env:K22_APP_ID --packVersion $env:K22_VERSION --packDir publish/app --mainExe $env:K22_MAIN_EXE --channel $env:K22_CHANNEL --icon $env:K22_ICON --outputDir Releases @sign
vpk pack --runtime $env:K22_RUNTIME --packId $env:K22_APP_ID --packVersion $env:K22_VERSION --packDir publish/app --mainExe $env:K22_MAIN_EXE --channel $env:K22_CHANNEL --icon $env:K22_ICON --outputDir Releases --instLocation PerUser @sign
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
# Keeps only what this build produced, as `vpk upload` does: the files listed in
@@ -534,6 +540,108 @@ jobs:
Write-Output "Assets: $($names -join ', '); feed: $feedName; setup: $setup"
"setup=$setup" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
- name: Extract required MSI payload
if: inputs.msi
id: msi
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$installers = @(Get-ChildItem -LiteralPath release -Filter '*.msi' -File)
if ($installers.Count -ne 1) { throw 'Exactly one MSI installer is required.' }
$root = Join-Path $env:RUNNER_TEMP ('k22-msi-' + [guid]::NewGuid().ToString('N'))
"root=$root" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
$image = Join-Path $root 'image'
$assets = Join-Path $root 'assets'
New-Item -ItemType Directory -Path $image, $assets -Force | Out-Null
$start = [Diagnostics.ProcessStartInfo]::new('msiexec.exe')
$start.UseShellExecute = $false
$start.CreateNoWindow = $true
foreach ($argument in @('/a', $installers[0].FullName, '/qn', "TARGETDIR=$image", '/l*v', (Join-Path $root 'extract.log')))
{
$start.ArgumentList.Add($argument)
}
$process = [Diagnostics.Process]::Start($start)
try
{
if (-not $process.WaitForExit(180000)) { $process.Kill(); throw 'MSI extraction timed out.' }
if ($process.ExitCode -ne 0) { throw "MSI extraction failed: $($process.ExitCode)." }
}
finally { $process.Dispose() }
$manifests = @(Get-ChildItem -LiteralPath $image -Recurse -File -Filter sq.version)
if ($manifests.Count -ne 1 -or $manifests[0].Directory.Name -ne 'current') { throw 'Expected one installed MSI application payload.' }
$payload = $manifests[0].Directory.Parent.FullName
[IO.Compression.ZipFile]::CreateFromDirectory($payload, (Join-Path $assets 'msi-Portable.zip'))
"directory=$assets" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
# Reuse the existing archive verifier for MSI's administratively extracted
# payload: every owned/third-party byte and generated launcher is checked.
- name: Verify exact signed MSI payload
if: inputs.msi && inputs.sign
uses: KofTwentyTwo/standards/.github/actions/windows-signing@f896215ec96854b18883191759259fd9866ee9be # reviewed signing helpers on main
with:
operation: verify-distribution
directory: ${{ steps.msi.outputs.directory }}
manifest: ${{ steps.signing.outputs.manifest }}
expected-subject: ${{ vars.ARTIFACT_SIGNING_SUBJECT || vars.TRUSTED_SIGNING_SUBJECT }}
- name: Verify MSI installs, launches and uninstalls
if: inputs.msi
shell: pwsh
env:
MSI_ROOT: ${{ steps.msi.outputs.root }}
run: |
$ErrorActionPreference = 'Stop'
$msi = @(Get-ChildItem -LiteralPath release -Filter '*.msi' -File)
if ($msi.Count -ne 1) { throw 'Exactly one MSI installer is required.' }
$installer = New-Object -ComObject WindowsInstaller.Installer
$database = $installer.OpenDatabase($msi[0].FullName, 0)
$view = $database.OpenView('SELECT `Value` FROM `Property` WHERE `Property` = ''ProductCode''')
$view.Execute()
$product = $view.Fetch().StringData(1)
if ($product -notmatch '^\{[0-9A-Fa-f-]{36}\}$') { throw 'Invalid MSI product identity.' }
$target = Join-Path $env:MSI_ROOT 'installed'
$application = $null
try
{
$install = Start-Process msiexec.exe -ArgumentList @('/i', ('"' + $msi[0].FullName + '"'), '/qn', ('VELOPACK_INSTALLDIR="' + $target + '"'), '/l*v', ('"' + (Join-Path $env:MSI_ROOT 'install.log') + '"')) -WindowStyle Hidden -PassThru
if (-not $install.WaitForExit(180000)) { $install.Kill(); throw 'MSI installation timed out.' }
if ($install.ExitCode -ne 0) { throw "MSI installation failed: $($install.ExitCode)." }
$executable = Join-Path $target "current/$env:K22_MAIN_EXE"
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) { throw 'MSI did not install the application.' }
$application = Start-Process -FilePath $executable -PassThru
$deadline = [DateTime]::UtcNow.AddSeconds(30)
do
{
$application.Refresh()
if ($application.HasExited) { throw 'MSI-installed application exited before opening its window.' }
if ($application.MainWindowHandle -ne [IntPtr]::Zero) { break }
Start-Sleep -Milliseconds 200
} while ([DateTime]::UtcNow -lt $deadline)
if ($application.MainWindowHandle -eq [IntPtr]::Zero) { throw 'MSI-installed application did not open a window.' }
if (-not $application.CloseMainWindow() -or -not $application.WaitForExit(10000)) { $application.Kill(); throw 'MSI-installed application did not close normally.' }
}
finally
{
if ($null -ne $application -and -not $application.HasExited) { $application.Kill() }
$uninstall = Start-Process msiexec.exe -ArgumentList @('/x', $product, '/qn', '/l*v', ('"' + (Join-Path $env:MSI_ROOT 'uninstall.log') + '"')) -WindowStyle Hidden -PassThru
if (-not $uninstall.WaitForExit(180000)) { $uninstall.Kill(); throw 'MSI uninstall timed out.' }
if ($uninstall.ExitCode -notin 0, 1605) { throw "MSI uninstall failed: $($uninstall.ExitCode)." }
}
if (Test-Path -LiteralPath (Join-Path $target "current/$env:K22_MAIN_EXE")) { throw 'MSI uninstall left the application installed.' }
Write-Output "MSI $product installed, opened its window, closed and uninstalled successfully."
- name: Remove temporary MSI extraction
if: always() && inputs.msi
shell: pwsh
env:
MSI_ROOT: ${{ steps.msi.outputs.root }}
run: |
if ([string]::IsNullOrWhiteSpace($env:MSI_ROOT)) { exit 0 }
$root = [IO.Path]::GetFullPath($env:MSI_ROOT)
$temporaryRoot = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath($env:RUNNER_TEMP))
if ([IO.Path]::GetDirectoryName($root) -ne $temporaryRoot -or [IO.Path]::GetFileName($root) -notmatch '^k22-msi-[0-9a-f]{32}$') { throw 'Unexpected MSI scratch directory.' }
if (Test-Path -LiteralPath $root) { Remove-Item -LiteralPath $root -Recurse -Force }
- name: Zip CLI
if: inputs.cli-project != ''
shell: pwsh
@@ -648,6 +756,13 @@ jobs:
subject-path: release/*-cli-*.zip
sbom-path: sbom/${{ inputs.app-id }}-cli-${{ needs.build.outputs.version }}.cdx.json
- name: Attest MSI SBOM
if: inputs.msi
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-path: release/*.msi
sbom-path: sbom/${{ inputs.app-id }}-${{ needs.build.outputs.version }}.cdx.json
- name: Upload SBOMs
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
+1 -1
View File
@@ -1 +1 @@
{".":"1.1.2"}
{".":"1.2.0"}
+11
View File
@@ -3,6 +3,17 @@
<!-- Generated by Release Please and GitHub; permit native spacing, links and repeated categories. -->
<!-- markdownlint-disable MD012 MD022 MD024 MD032 MD034 -->
## 1.2.0 (2026-10-13)
<!-- Release notes generated using configuration in .github/release.yml at main -->
## What's Changed
### Changes
* feat(releases): publish signed per-user MSI installers by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/47
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.2...v1.2.0
## 1.1.2 (2026-10-13)
<!-- Release notes generated using configuration in .github/release.yml at main -->
+1 -1
View File
@@ -1 +1 @@
1.1.2
1.2.0