Add production endpoint verifier
This commit is contained in:
@@ -37,6 +37,8 @@ Date: 2026-06-08.
|
||||
- Windows/Desktop update channel defaults now match the build channel: stable builds default to `stable`, beta builds default to `beta`.
|
||||
- Windows/Desktop CI now validates production release invariants before Windows packaging and requires signing variables for stable tag releases.
|
||||
- Windows/Desktop now has a configured HTTPS update feed at `https://qsfera.kusoft.xyz/desktop/updates/stable.xml`; the feed returned HTTP `200` on 2026-06-08 and contains the XML elements parsed by `Desktop/src/gui/updater/updateinfo.cpp`.
|
||||
- Added `scripts/verify-production-state.ps1` as a repeatable live production endpoint gate for the public server status, Android Argus manifest/download, and Desktop update feed.
|
||||
- `scripts/verify-production-state.ps1 -VerifyAndroidDownload` passed on 2026-06-08: server `edition=stable`/`productversion=7.0.0`, Android `1.3.6`/`34`, downloaded APK size `12217032`, APK SHA-256 `921f2dad21aa85991a30ab5e7792f79c32f87ba5bb14188185ff2e6a438b4428`, and valid Desktop no-update feed.
|
||||
|
||||
## Raspberry Pi 5 Check
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
param(
|
||||
[string]$ServerStatusUrl = "https://qsfera.kusoft.xyz/status.php",
|
||||
[string]$ExpectedServerVersion = "7.0.0",
|
||||
[string]$ExpectedServerEdition = "stable",
|
||||
|
||||
[string]$AndroidManifestUrl = "https://argus.kusoft.xyz/api/apps/qsfera-mobile/manifest?platform=android&channel=stable",
|
||||
[string]$ExpectedAndroidSlug = "qsfera-mobile",
|
||||
[string]$ExpectedAndroidPackageName = "eu.qsfera.android",
|
||||
[string]$ExpectedAndroidVersion = "1.3.6",
|
||||
[int]$ExpectedAndroidVersionCode = 34,
|
||||
[string]$ExpectedAndroidChannel = "stable",
|
||||
[string]$ExpectedAndroidPlatform = "android",
|
||||
[string]$ExpectedAndroidPackageKind = "apk",
|
||||
[string]$ExpectedAndroidSha256 = "921f2dad21aa85991a30ab5e7792f79c32f87ba5bb14188185ff2e6a438b4428",
|
||||
[string]$AndroidDownloadUrl = "https://argus.kusoft.xyz/api/apps/qsfera-mobile/download/latest?platform=android&channel=stable",
|
||||
[switch]$VerifyAndroidDownload,
|
||||
|
||||
[string]$DesktopUpdateFeedUrl = "https://qsfera.kusoft.xyz/desktop/updates/stable.xml",
|
||||
[switch]$RequireDesktopUpdate
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Fail($Message) {
|
||||
Write-Error $Message
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Assert-AbsoluteHttpsUrl($Name, $Url) {
|
||||
$parsedUrl = $null
|
||||
if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$parsedUrl)) {
|
||||
Fail "$Name must be an absolute URL: $Url"
|
||||
}
|
||||
|
||||
if ($parsedUrl.Scheme -ne "https") {
|
||||
Fail "$Name must use HTTPS: $Url"
|
||||
}
|
||||
}
|
||||
|
||||
function Assert-Equal($Name, $Actual, $Expected) {
|
||||
if ([string]$Actual -ne [string]$Expected) {
|
||||
Fail "$Name expected '$Expected' but got '$Actual'."
|
||||
}
|
||||
|
||||
Write-Host "OK: $Name = $Actual"
|
||||
}
|
||||
|
||||
function Assert-True($Name, [bool]$Value) {
|
||||
if (-not $Value) {
|
||||
Fail "$Name expected True but got False."
|
||||
}
|
||||
|
||||
Write-Host "OK: $Name = True"
|
||||
}
|
||||
|
||||
function Invoke-CheckedWebRequest($Name, $Url) {
|
||||
Assert-AbsoluteHttpsUrl $Name $Url
|
||||
|
||||
try {
|
||||
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 30
|
||||
} catch {
|
||||
Fail "$Name request failed for ${Url}: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($response.StatusCode -ne 200) {
|
||||
Fail "$Name must return HTTP 200. Actual status: $($response.StatusCode)."
|
||||
}
|
||||
|
||||
Write-Host "OK: $Name returned HTTP 200"
|
||||
return $response
|
||||
}
|
||||
|
||||
function Convert-JsonContent($Name, $Content) {
|
||||
try {
|
||||
return $Content | ConvertFrom-Json -ErrorAction Stop
|
||||
} catch {
|
||||
Fail "$Name returned invalid JSON: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RequiredProperty($Object, $Name, $Context) {
|
||||
$property = $Object.PSObject.Properties[$Name]
|
||||
if ($null -eq $property) {
|
||||
Fail "$Context is missing '$Name'."
|
||||
}
|
||||
|
||||
return $property.Value
|
||||
}
|
||||
|
||||
function Test-Sha256String($Value) {
|
||||
return [regex]::IsMatch($Value, "^[0-9a-f]{64}$")
|
||||
}
|
||||
|
||||
Write-Host "Checking QSfera production server status..."
|
||||
$serverResponse = Invoke-CheckedWebRequest "Server status endpoint" $ServerStatusUrl
|
||||
$serverStatus = Convert-JsonContent "Server status endpoint" $serverResponse.Content
|
||||
Assert-True "Server installed" ([bool](Get-RequiredProperty $serverStatus "installed" "Server status"))
|
||||
Assert-Equal "Server maintenance" (Get-RequiredProperty $serverStatus "maintenance" "Server status") "False"
|
||||
Assert-Equal "Server needsDbUpgrade" (Get-RequiredProperty $serverStatus "needsDbUpgrade" "Server status") "False"
|
||||
Assert-Equal "Server edition" (Get-RequiredProperty $serverStatus "edition" "Server status") $ExpectedServerEdition
|
||||
Assert-Equal "Server productversion" (Get-RequiredProperty $serverStatus "productversion" "Server status") $ExpectedServerVersion
|
||||
|
||||
Write-Host "Checking QSfera Android Argus manifest..."
|
||||
$manifestResponse = Invoke-CheckedWebRequest "Android Argus manifest" $AndroidManifestUrl
|
||||
$manifest = Convert-JsonContent "Android Argus manifest" $manifestResponse.Content
|
||||
Assert-Equal "Android app slug" (Get-RequiredProperty $manifest "slug" "Android manifest") $ExpectedAndroidSlug
|
||||
Assert-Equal "Android package name" (Get-RequiredProperty $manifest "androidPackageName" "Android manifest") $ExpectedAndroidPackageName
|
||||
|
||||
$release = Get-RequiredProperty $manifest "release" "Android manifest"
|
||||
if ($null -eq $release) {
|
||||
Fail "Android manifest release must not be null."
|
||||
}
|
||||
|
||||
Assert-Equal "Android release version" (Get-RequiredProperty $release "version" "Android release") $ExpectedAndroidVersion
|
||||
Assert-Equal "Android versionCode" (Get-RequiredProperty $release "androidVersionCode" "Android release") $ExpectedAndroidVersionCode
|
||||
Assert-Equal "Android release channel" (Get-RequiredProperty $release "channel" "Android release") $ExpectedAndroidChannel
|
||||
Assert-Equal "Android release platform" (Get-RequiredProperty $release "platform" "Android release") $ExpectedAndroidPlatform
|
||||
Assert-Equal "Android package kind" (Get-RequiredProperty $release "packageKind" "Android release") $ExpectedAndroidPackageKind
|
||||
$releaseSha256 = Get-RequiredProperty $release "sha256" "Android release"
|
||||
Assert-True "Android SHA-256 shape" (Test-Sha256String $releaseSha256)
|
||||
Assert-Equal "Android SHA-256" $releaseSha256 $ExpectedAndroidSha256
|
||||
|
||||
if ($VerifyAndroidDownload) {
|
||||
Write-Host "Checking QSfera Android latest APK download..."
|
||||
Assert-AbsoluteHttpsUrl "Android download URL" $AndroidDownloadUrl
|
||||
$tempFile = [System.IO.Path]::GetTempFileName()
|
||||
try {
|
||||
try {
|
||||
Invoke-WebRequest -Uri $AndroidDownloadUrl -UseBasicParsing -TimeoutSec 120 -OutFile $tempFile | Out-Null
|
||||
} catch {
|
||||
Fail "Android download request failed for ${AndroidDownloadUrl}: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
$downloadSize = (Get-Item -LiteralPath $tempFile).Length
|
||||
$expectedSize = Get-RequiredProperty $release "packageSizeBytes" "Android release"
|
||||
Assert-Equal "Android APK size" $downloadSize $expectedSize
|
||||
|
||||
$downloadSha256 = (Get-FileHash -Algorithm SHA256 -LiteralPath $tempFile).Hash.ToLowerInvariant()
|
||||
Assert-Equal "Android APK download SHA-256" $downloadSha256 $ExpectedAndroidSha256
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $tempFile -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Checking QSfera Desktop update feed..."
|
||||
$desktopResponse = Invoke-CheckedWebRequest "Desktop update feed" $DesktopUpdateFeedUrl
|
||||
try {
|
||||
[xml]$desktopFeed = $desktopResponse.Content
|
||||
} catch {
|
||||
Fail "Desktop update feed returned invalid XML: $($_.Exception.Message)"
|
||||
}
|
||||
|
||||
if ($desktopFeed.DocumentElement.Name -ne "QSfera") {
|
||||
Fail "Desktop update feed root element must be <QSfera>."
|
||||
}
|
||||
|
||||
foreach ($elementName in @("version", "versionstring", "web", "downloadurl")) {
|
||||
if ($null -eq $desktopFeed.QSfera.$elementName) {
|
||||
Fail "Desktop update feed is missing <$elementName>."
|
||||
}
|
||||
}
|
||||
|
||||
$desktopWeb = [string]$desktopFeed.QSfera.web
|
||||
if (-not [string]::IsNullOrWhiteSpace($desktopWeb)) {
|
||||
Assert-AbsoluteHttpsUrl "Desktop update feed <web>" $desktopWeb
|
||||
}
|
||||
|
||||
$desktopVersion = [string]$desktopFeed.QSfera.version
|
||||
$desktopVersionString = [string]$desktopFeed.QSfera.versionstring
|
||||
$desktopDownloadUrl = [string]$desktopFeed.QSfera.downloadurl
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($desktopVersion)) {
|
||||
if ($RequireDesktopUpdate) {
|
||||
Fail "Desktop update feed must advertise a concrete version when -RequireDesktopUpdate is set."
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($desktopDownloadUrl)) {
|
||||
Fail "Desktop no-update feed must leave <downloadurl> empty when <version> is empty."
|
||||
}
|
||||
|
||||
Write-Host "OK: Desktop update feed is a valid no-update feed"
|
||||
} else {
|
||||
$parsedVersion = $null
|
||||
if (-not [System.Version]::TryParse($desktopVersion, [ref]$parsedVersion)) {
|
||||
Fail "Desktop update feed <version> must be parseable as a version number."
|
||||
}
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($desktopVersionString)) {
|
||||
Fail "Desktop update feed must set <versionstring> when <version> is present."
|
||||
}
|
||||
|
||||
Assert-AbsoluteHttpsUrl "Desktop update feed <downloadurl>" $desktopDownloadUrl
|
||||
$downloadUri = [System.Uri]$desktopDownloadUrl
|
||||
if (-not $downloadUri.AbsolutePath.EndsWith(".msi", [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
Fail "Desktop Windows updater expects an MSI download URL."
|
||||
}
|
||||
|
||||
Write-Host "OK: Desktop update feed advertises version $desktopVersionString"
|
||||
}
|
||||
|
||||
Write-Host "QSfera production endpoint verification passed."
|
||||
Reference in New Issue
Block a user