Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ef3d319b8 | ||
|
|
7584fa19cc | ||
|
|
083d90326a | ||
|
|
261a0055ae | ||
|
|
96cbdf208a | ||
|
|
b3e4b4989c | ||
|
|
089a94ac3c |
No files matched your search
@@ -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).
|
||||
@@ -366,6 +370,37 @@ jobs:
|
||||
tenant-id: ${{ vars.AZURE_TENANT_ID }}
|
||||
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
|
||||
|
||||
# The pinned official action installs this tool with --add-source, which
|
||||
# conflicts with NuGet package source mapping. Stage its verified package
|
||||
# in the action's own dependency directory without changing caller policy.
|
||||
- name: Prepare pinned Artifact Signing CLI dependency
|
||||
if: inputs.sign
|
||||
shell: pwsh
|
||||
run: |
|
||||
& {
|
||||
param(
|
||||
[string]$PackageDirectory = $env:RUNNER_TEMP,
|
||||
[string]$InstallRoot = $env:LOCALAPPDATA
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$version = '0.9.1-beta.26227.3'
|
||||
$package = Join-Path $PackageDirectory "sign.$version.nupkg"
|
||||
$destination = Join-Path $InstallRoot "ArtifactSigning/sign/sign.$version"
|
||||
if (Test-Path -LiteralPath $destination) { throw 'Artifact Signing CLI directory already exists; use a fresh release runner.' }
|
||||
Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/sign/$version/sign.$version.nupkg" -OutFile $package
|
||||
if ((Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash -ne '34FD0D4AEABDBC363A48883881865B4DD65E6C11BA916028082FA23F3E1B1BA1')
|
||||
{
|
||||
throw 'Artifact Signing CLI package checksum mismatch.'
|
||||
}
|
||||
dotnet nuget verify $package --all
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Artifact Signing CLI NuGet signature verification failed.' }
|
||||
[IO.Compression.ZipFile]::ExtractToDirectory($package, $destination)
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $destination 'tools/net8.0/any/sign.dll') -PathType Leaf))
|
||||
{
|
||||
throw 'Artifact Signing CLI package is missing its executable.'
|
||||
}
|
||||
} @args
|
||||
|
||||
# Signs the app's own executables and DLLs (and the CLI) before packing, so the
|
||||
# binaries inside the installer, the portable zip, and the update packages are
|
||||
# all signed (K22-REL-05).
|
||||
@@ -463,6 +498,8 @@ jobs:
|
||||
|
||||
- name: Velopack pack
|
||||
shell: pwsh
|
||||
env:
|
||||
VPK_MSI: ${{ inputs.msi }}
|
||||
run: |
|
||||
$sign = @()
|
||||
if ($env:K22_SIGN -eq 'true')
|
||||
@@ -474,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
|
||||
@@ -503,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
|
||||
@@ -617,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 @@
|
||||
{".":"1.1.0"}
|
||||
{".":"1.2.0"}
|
||||
@@ -3,6 +3,40 @@
|
||||
<!-- 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 -->
|
||||
|
||||
## What's Changed
|
||||
### Changes
|
||||
* fix(releases): scope bootstrap parameters under Actions pwsh by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/45
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.1...v1.1.2
|
||||
|
||||
## 1.1.1 (2026-10-12)
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
## What's Changed
|
||||
### Changes
|
||||
* docs(inventory): register MarkdownViewr and the Windows starter by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/36
|
||||
* fix(releases): bootstrap verified Artifact Signing dependency by @KofTwentyTwo in https://github.com/KofTwentyTwo/standards/pull/44
|
||||
|
||||
|
||||
**Full Changelog**: https://github.com/KofTwentyTwo/standards/compare/v1.1.0...v1.1.1
|
||||
|
||||
## 1.1.0 (2026-10-12)
|
||||
|
||||
<!-- Release notes generated using configuration in .github/release.yml at main -->
|
||||
|
||||
@@ -24,6 +24,8 @@ conformance version in its README once it passes the conformance checker.
|
||||
| --- | --- | --- | --- |
|
||||
| [standards](https://github.com/KofTwentyTwo/standards) | These policies, standards, configs, and reusable workflows | Product | Not yet: rulesets and CI being added |
|
||||
| [AppKit](https://github.com/KofTwentyTwo/AppKit) | Shared foundation packages for Windows desktop apps | Product | Adoption in progress |
|
||||
| [MarkdownViewr.Windows](https://github.com/KofTwentyTwo/MarkdownViewr.Windows) | Windows Markdown reader based on Darin Kelkhoff's macOS markdownViewr and AppKit | Product | Adoption in progress: release acceptance, signing and maintainer review pending |
|
||||
| [windows-app-template](https://github.com/KofTwentyTwo/windows-app-template) | Reusable AppKit Windows app starter with optional CLI and stable/dev release workflows | Product | Adoption in progress: shared defaults and release controls in review |
|
||||
| [gclo](https://github.com/KofTwentyTwo/gclo) | Clone and update every repository of a GitHub organization (Windows app and CLI) | Product | Adoption in progress: standards gates, governance files, and the Kingsrook layout are in review (gclo #62); settings and exceptions pending |
|
||||
|
||||
### Awaiting classification
|
||||
|
||||
@@ -14,6 +14,14 @@ BeforeAll `
|
||||
if($start -lt 0 -or $end -ge $lines.Count) { throw 'Cannot locate the actual Velopack signing wrapper.' }
|
||||
$script:Wrapper = Join-Path $TestDrive 'sign-one.ps1'
|
||||
[IO.File]::WriteAllLines($script:Wrapper, @($lines[($start + 1)..($end - 1)] | ForEach-Object { $_.Substring(10) }))
|
||||
$dependencyStart = [Array]::IndexOf($lines, ' - name: Prepare pinned Artifact Signing CLI dependency')
|
||||
if($dependencyStart -lt 0) { throw 'Cannot locate the actual signing dependency bootstrap.' }
|
||||
$dependencyStart += 4
|
||||
$dependencyEnd = $dependencyStart
|
||||
while($dependencyEnd -lt $lines.Count -and $lines[$dependencyEnd].StartsWith(' ')) { $dependencyEnd++ }
|
||||
$script:Bootstrap = Join-Path $TestDrive 'sign-dependency.ps1'
|
||||
# GitHub's default pwsh shell prepends this statement to every inline run body.
|
||||
[IO.File]::WriteAllLines($script:Bootstrap, @('$ErrorActionPreference = "stop"') + @($lines[$dependencyStart..($dependencyEnd - 1)] | ForEach-Object { $_.Substring(10) }))
|
||||
$script:OldTool = $env:K22_VPK_SIGN_TOOL
|
||||
$env:K22_VPK_SIGN_TOOL = 'Invoke-WrapperTool'
|
||||
|
||||
@@ -31,6 +39,69 @@ BeforeAll `
|
||||
}
|
||||
}
|
||||
|
||||
Describe 'The actual Artifact Signing dependency bootstrap' `
|
||||
{
|
||||
BeforeEach `
|
||||
{
|
||||
$script:InstallRoot = Join-Path $TestDrive ([guid]::NewGuid().ToString('N'))
|
||||
$script:PackageDirectory = Join-Path $script:InstallRoot 'downloads'
|
||||
[void][IO.Directory]::CreateDirectory($script:PackageDirectory)
|
||||
$script:Destination = Join-Path $script:InstallRoot 'ArtifactSigning/sign/sign.0.9.1-beta.26227.3'
|
||||
Mock Invoke-WebRequest `
|
||||
{
|
||||
$fixture = Join-Path (Split-Path -Parent $OutFile) 'fixture'
|
||||
[void][IO.Directory]::CreateDirectory((Join-Path $fixture 'tools/net8.0/any'))
|
||||
[IO.File]::WriteAllText((Join-Path $fixture 'tools/net8.0/any/sign.dll'), 'fixture')
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($fixture, $OutFile)
|
||||
}
|
||||
Mock Get-FileHash { [pscustomobject]@{ Hash = '34FD0D4AEABDBC363A48883881865B4DD65E6C11BA916028082FA23F3E1B1BA1' } }
|
||||
Mock dotnet { $global:LASTEXITCODE = 0 }
|
||||
}
|
||||
|
||||
It 'stages the verified tool where the official action finds it' `
|
||||
{
|
||||
& $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory
|
||||
Get-Content -LiteralPath (Join-Path $script:Destination 'tools/net8.0/any/sign.dll') | Should -Be 'fixture'
|
||||
Should -Invoke dotnet -Exactly 1
|
||||
}
|
||||
|
||||
It 'rejects a changed download before verification or extraction' `
|
||||
{
|
||||
Mock Get-FileHash { [pscustomobject]@{ Hash = 'bad' } }
|
||||
{ & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*checksum mismatch*'
|
||||
Test-Path -LiteralPath $script:Destination | Should -BeFalse
|
||||
Should -Invoke dotnet -Exactly 0
|
||||
}
|
||||
|
||||
It 'rejects an invalid NuGet signature before extraction' `
|
||||
{
|
||||
Mock dotnet { $global:LASTEXITCODE = 1 }
|
||||
{ & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*signature verification failed*'
|
||||
Test-Path -LiteralPath $script:Destination | Should -BeFalse
|
||||
}
|
||||
|
||||
It 'preserves a preexisting dependency directory' `
|
||||
{
|
||||
[void][IO.Directory]::CreateDirectory($script:Destination)
|
||||
$marker = Join-Path $script:Destination 'marker.txt'
|
||||
[IO.File]::WriteAllText($marker, 'preserve')
|
||||
{ & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*already exists*'
|
||||
Get-Content -LiteralPath $marker | Should -Be 'preserve'
|
||||
Should -Invoke Invoke-WebRequest -Exactly 0
|
||||
}
|
||||
|
||||
It 'rejects a package without the expected executable' `
|
||||
{
|
||||
Mock Invoke-WebRequest `
|
||||
{
|
||||
$emptyFixture = Join-Path (Split-Path -Parent $OutFile) 'empty'
|
||||
[void][IO.Directory]::CreateDirectory($emptyFixture)
|
||||
[IO.Compression.ZipFile]::CreateFromDirectory($emptyFixture, $OutFile)
|
||||
}
|
||||
{ & $script:Bootstrap -InstallRoot $script:InstallRoot -PackageDirectory $script:PackageDirectory } | Should -Throw '*missing its executable*'
|
||||
}
|
||||
}
|
||||
|
||||
AfterAll `
|
||||
{
|
||||
$env:K22_VPK_SIGN_TOOL = $script:OldTool
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
1.1.0
|
||||
1.2.0
|
||||
Reference in new issue
Block a user