Add safe Windows desktop feed publication
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
param(
|
||||
[string]$MsiPath = (Join-Path $PSScriptRoot "..\artifacts\windows-desktop\QSfera-Desktop-4.0.0-win-x64.msi"),
|
||||
[string]$Version,
|
||||
[string]$Channel = "stable",
|
||||
[string]$PublicBaseUrl = "https://qsfera.kusoft.xyz",
|
||||
[string]$OutputRoot = (Join-Path $PSScriptRoot "..\artifacts\windows-desktop\publish"),
|
||||
[string]$RemoteHost,
|
||||
[string]$RemoteUser = "sevenhill",
|
||||
[int]$RemotePort = 22,
|
||||
[string]$RemoteDownloadDirectory = "/var/www/qsfera/desktop/downloads",
|
||||
[string]$RemoteFeedPath = "/var/www/qsfera/desktop/updates/stable.xml",
|
||||
[switch]$SkipSignatureCheck
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Fail([string]$Message) {
|
||||
Write-Error $Message
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Assert-HttpsBaseUrl([string]$Url) {
|
||||
$uri = $null
|
||||
if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$uri) -or $uri.Scheme -ne "https") {
|
||||
Fail "PublicBaseUrl must be an absolute HTTPS URL: $Url"
|
||||
}
|
||||
return $uri.AbsoluteUri.TrimEnd("/")
|
||||
}
|
||||
|
||||
function Get-QSferaDesktopVersion {
|
||||
$versionFile = Join-Path $PSScriptRoot "..\Desktop\VERSION.cmake"
|
||||
if (-not (Test-Path -LiteralPath $versionFile)) {
|
||||
Fail "Desktop version file was not found: $versionFile"
|
||||
}
|
||||
|
||||
$values = @{}
|
||||
foreach ($line in Get-Content -LiteralPath $versionFile) {
|
||||
if ($line -match '^\s*set\(\s*(MIRALL_VERSION_(MAJOR|MINOR|PATCH|BUILD))\s+"?([^"\s\)]+)"?\s*\)') {
|
||||
$values[$matches[1]] = $matches[3]
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($key in @("MIRALL_VERSION_MAJOR", "MIRALL_VERSION_MINOR", "MIRALL_VERSION_PATCH")) {
|
||||
if (-not $values.ContainsKey($key)) {
|
||||
Fail "Desktop version file does not define $key."
|
||||
}
|
||||
}
|
||||
|
||||
$baseVersion = "{0}.{1}.{2}" -f $values["MIRALL_VERSION_MAJOR"], $values["MIRALL_VERSION_MINOR"], $values["MIRALL_VERSION_PATCH"]
|
||||
$build = $values["MIRALL_VERSION_BUILD"]
|
||||
if ([string]::IsNullOrWhiteSpace($build) -or $build -eq "0") {
|
||||
return $baseVersion
|
||||
}
|
||||
return "$baseVersion.$build"
|
||||
}
|
||||
|
||||
function Quote-RemotePath([string]$Path) {
|
||||
return "'" + ($Path -replace "'", "'\''") + "'"
|
||||
}
|
||||
|
||||
function Get-RemoteParentPath([string]$Path) {
|
||||
$index = $Path.LastIndexOf("/")
|
||||
if ($index -le 0) {
|
||||
return "/"
|
||||
}
|
||||
return $Path.Substring(0, $index)
|
||||
}
|
||||
|
||||
function Invoke-CheckedNative([string]$Program, [string[]]$Arguments) {
|
||||
& $Program @Arguments
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Fail "$Program failed with exit code $LASTEXITCODE."
|
||||
}
|
||||
}
|
||||
|
||||
$resolvedMsi = $null
|
||||
try {
|
||||
$resolvedMsi = (Resolve-Path -LiteralPath $MsiPath).Path
|
||||
} catch {
|
||||
Fail "MSI file was not found: $MsiPath"
|
||||
}
|
||||
|
||||
if (-not $resolvedMsi.EndsWith(".msi", [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
Fail "Windows desktop publication expects an .msi file: $resolvedMsi"
|
||||
}
|
||||
|
||||
$hasRemoteTarget = -not [string]::IsNullOrWhiteSpace($RemoteHost)
|
||||
if ($hasRemoteTarget -and $SkipSignatureCheck) {
|
||||
Fail "Remote Windows desktop publication cannot use -SkipSignatureCheck."
|
||||
}
|
||||
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $resolvedMsi
|
||||
if (-not $SkipSignatureCheck -and $signature.Status -ne "Valid") {
|
||||
Fail "MSI must be Authenticode-signed before Windows production publication. Actual status: $($signature.Status)."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||
$Version = Get-QSferaDesktopVersion
|
||||
}
|
||||
|
||||
$parsedVersion = $null
|
||||
if (-not [System.Version]::TryParse($Version, [ref]$parsedVersion)) {
|
||||
Fail "Version must be parseable as a Windows updater version: $Version"
|
||||
}
|
||||
|
||||
if ($Channel -ne "stable") {
|
||||
Fail "Only the stable desktop update feed is configured for production publication. Actual channel: $Channel"
|
||||
}
|
||||
|
||||
$publicBase = Assert-HttpsBaseUrl $PublicBaseUrl
|
||||
$msiName = Split-Path -Leaf $resolvedMsi
|
||||
$downloadUrl = "$publicBase/desktop/downloads/$msiName"
|
||||
$feedPath = Join-Path (New-Item -ItemType Directory -Path $OutputRoot -Force).FullName "$Channel.xml"
|
||||
$sha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $resolvedMsi).Hash.ToLowerInvariant()
|
||||
$size = (Get-Item -LiteralPath $resolvedMsi).Length
|
||||
|
||||
$feed = @"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<QSfera>
|
||||
<version>$Version</version>
|
||||
<versionstring>$Version</versionstring>
|
||||
<web>$publicBase/</web>
|
||||
<downloadurl>$downloadUrl</downloadurl>
|
||||
</QSfera>
|
||||
"@
|
||||
|
||||
$feed | Set-Content -LiteralPath $feedPath -Encoding UTF8
|
||||
|
||||
$feedValidator = Join-Path $PSScriptRoot "..\Desktop\scripts\validate-desktop-update-feed.ps1"
|
||||
if (-not (Test-Path -LiteralPath $feedValidator)) {
|
||||
Fail "Desktop update feed validator was not found: $feedValidator"
|
||||
}
|
||||
& $feedValidator -Path $feedPath
|
||||
if (-not $?) {
|
||||
Fail "Generated desktop update feed failed validation."
|
||||
}
|
||||
|
||||
Write-Output "QSfera Windows desktop publication prepared."
|
||||
Write-Output "MSI: $resolvedMsi"
|
||||
Write-Output "Version: $Version"
|
||||
Write-Output "Size: $size"
|
||||
Write-Output "SHA-256: $sha256"
|
||||
Write-Output "Feed: $feedPath"
|
||||
Write-Output "Download URL: $downloadUrl"
|
||||
Write-Output "Signature: $($signature.Status)"
|
||||
|
||||
if (-not $hasRemoteTarget) {
|
||||
Write-Output "Remote upload skipped: set -RemoteHost after signing the MSI."
|
||||
exit 0
|
||||
}
|
||||
|
||||
$scp = Get-Command scp -ErrorAction SilentlyContinue
|
||||
$ssh = Get-Command ssh -ErrorAction SilentlyContinue
|
||||
if (-not $scp -or -not $ssh) {
|
||||
Fail "Both scp and ssh must be available for remote Windows desktop publication."
|
||||
}
|
||||
|
||||
$remote = "$RemoteUser@$RemoteHost"
|
||||
$remoteMsiTmp = "$RemoteDownloadDirectory/$msiName.tmp"
|
||||
$remoteMsiFinal = "$RemoteDownloadDirectory/$msiName"
|
||||
$remoteFeedTmp = "$RemoteFeedPath.tmp"
|
||||
$sshPort = [string]$RemotePort
|
||||
|
||||
$remoteFeedDirectory = Get-RemoteParentPath $RemoteFeedPath
|
||||
$prepareCommand = @(
|
||||
"set -e",
|
||||
"mkdir -p $(Quote-RemotePath $RemoteDownloadDirectory)",
|
||||
"mkdir -p $(Quote-RemotePath $remoteFeedDirectory)"
|
||||
) -join " && "
|
||||
|
||||
Invoke-CheckedNative $ssh.Source @("-p", $sshPort, $remote, $prepareCommand)
|
||||
Invoke-CheckedNative $scp.Source @("-P", $sshPort, $resolvedMsi, "${remote}:$remoteMsiTmp")
|
||||
Invoke-CheckedNative $scp.Source @("-P", $sshPort, $feedPath, "${remote}:$remoteFeedTmp")
|
||||
|
||||
$remoteCommand = @(
|
||||
"set -e",
|
||||
"test -s $(Quote-RemotePath $remoteMsiTmp)",
|
||||
"test -s $(Quote-RemotePath $remoteFeedTmp)",
|
||||
"mv -f $(Quote-RemotePath $remoteMsiTmp) $(Quote-RemotePath $remoteMsiFinal)",
|
||||
"mv -f $(Quote-RemotePath $remoteFeedTmp) $(Quote-RemotePath $RemoteFeedPath)"
|
||||
) -join " && "
|
||||
|
||||
Invoke-CheckedNative $ssh.Source @("-p", $sshPort, $remote, $remoteCommand)
|
||||
|
||||
& $feedValidator -Url "$publicBase/desktop/updates/$Channel.xml"
|
||||
if (-not $?) {
|
||||
Fail "Live desktop update feed failed validation after upload."
|
||||
}
|
||||
|
||||
Write-Output "QSfera Windows desktop publication uploaded."
|
||||
Reference in New Issue
Block a user