From 955be5134dcc2357ac57962284f385899b9bace0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Mon, 8 Jun 2026 21:18:46 +0300 Subject: [PATCH] Configure desktop update feed --- Desktop/QSFERA.cmake | 1 + Desktop/docs/windows-update-feed.md | 47 ++++++++++ .../scripts/validate-desktop-update-feed.ps1 | 85 +++++++++++++++++++ .../scripts/validate-production-release.ps1 | 16 ++++ PRODUCTION_READINESS.md | 3 +- 5 files changed, 151 insertions(+), 1 deletion(-) create mode 100644 Desktop/docs/windows-update-feed.md create mode 100644 Desktop/scripts/validate-desktop-update-feed.ps1 diff --git a/Desktop/QSFERA.cmake b/Desktop/QSFERA.cmake index 5034b3e6..6ac446a6 100644 --- a/Desktop/QSFERA.cmake +++ b/Desktop/QSFERA.cmake @@ -5,6 +5,7 @@ set( APPLICATION_VENDOR "QSfera" ) set( APPLICATION_ICON_NAME "qsfera" ) set( APPLICATION_REV_DOMAIN "eu.qsfera.desktop" ) set( APPLICATION_DEFAULT_SERVER_URL "https://qsfera.kusoft.xyz" ) +set( APPLICATION_UPDATE_URL "https://qsfera.kusoft.xyz/desktop/updates/stable.xml" ) if(BETA_CHANNEL_BUILD) set( APPLICATION_NAME "${APPLICATION_NAME} Beta") diff --git a/Desktop/docs/windows-update-feed.md b/Desktop/docs/windows-update-feed.md new file mode 100644 index 00000000..720b85c6 --- /dev/null +++ b/Desktop/docs/windows-update-feed.md @@ -0,0 +1,47 @@ +# Windows Update Feed + +QSfera Desktop uses the built-in Windows updater from `src/gui/updater`. +The updater reads `APPLICATION_UPDATE_URL`, appends query parameters such as +`version`, `platform=win32`, and `msi=true`, and expects an XML document with +root element `QSfera`. + +Production feed URL: + +```text +https://qsfera.kusoft.xyz/desktop/updates/stable.xml +``` + +The no-update feed is valid when no signed Windows MSI is available yet: + +```xml + + + + + https://qsfera.kusoft.xyz/ + + +``` + +To advertise a signed MSI update, publish the MSI over HTTPS and replace the +feed with a concrete version: + +```xml + + + 1.3.7 + 1.3.7 + https://qsfera.kusoft.xyz/ + https://qsfera.kusoft.xyz/desktop/downloads/QSfera_Desktop-1.3.7-x64.msi + +``` + +Validate the live feed before a Windows production release: + +```powershell +./scripts/validate-desktop-update-feed.ps1 -Url "https://qsfera.kusoft.xyz/desktop/updates/stable.xml" +./scripts/validate-production-release.ps1 -VerifyUpdateFeed +``` + +For a stable signed tag release, `validate-production-release.ps1 -RequireSigning` +also verifies this live feed. diff --git a/Desktop/scripts/validate-desktop-update-feed.ps1 b/Desktop/scripts/validate-desktop-update-feed.ps1 new file mode 100644 index 00000000..213f8c8d --- /dev/null +++ b/Desktop/scripts/validate-desktop-update-feed.ps1 @@ -0,0 +1,85 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Url +) + +$ErrorActionPreference = "Stop" + +function Fail($Message) { + Write-Error $Message + exit 1 +} + +$parsedUrl = $null +if (-not [System.Uri]::TryCreate($Url, [System.UriKind]::Absolute, [ref]$parsedUrl)) { + Fail "Desktop update feed URL is not absolute: $Url" +} + +if ($parsedUrl.Scheme -ne "https") { + Fail "Desktop update feed URL must use HTTPS: $Url" +} + +try { + $response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 30 +} catch { + Fail "Desktop update feed request failed for ${Url}: $($_.Exception.Message)" +} + +if ($response.StatusCode -ne 200) { + Fail "Desktop update feed must return HTTP 200. Actual status: $($response.StatusCode)" +} + +try { + [xml]$xml = $response.Content +} catch { + Fail "Desktop update feed is not valid XML: $($_.Exception.Message)" +} + +if ($xml.DocumentElement.Name -ne "QSfera") { + Fail "Desktop update feed root element must be ." +} + +$requiredElements = @("version", "versionstring", "web", "downloadurl") +foreach ($name in $requiredElements) { + if ($null -eq $xml.QSfera.$name) { + Fail "Desktop update feed is missing <$name>." + } +} + +$version = [string]$xml.QSfera.version +$versionString = [string]$xml.QSfera.versionstring +$web = [string]$xml.QSfera.web +$downloadUrl = [string]$xml.QSfera.downloadurl + +if (-not [string]::IsNullOrWhiteSpace($web)) { + $webUri = $null + if (-not [System.Uri]::TryCreate($web, [System.UriKind]::Absolute, [ref]$webUri) -or $webUri.Scheme -ne "https") { + Fail "Desktop update feed must be an absolute HTTPS URL when present." + } +} + +if ([string]::IsNullOrWhiteSpace($version)) { + if (-not [string]::IsNullOrWhiteSpace($downloadUrl)) { + Fail "Desktop no-update feed must leave empty when is empty." + } +} else { + $parsedVersion = $null + if (-not [System.Version]::TryParse($version, [ref]$parsedVersion)) { + Fail "Desktop update feed must be parseable as a version number." + } + + if ([string]::IsNullOrWhiteSpace($versionString)) { + Fail "Desktop update feed must set when is present." + } + + $downloadUri = $null + if (-not [System.Uri]::TryCreate($downloadUrl, [System.UriKind]::Absolute, [ref]$downloadUri) -or $downloadUri.Scheme -ne "https") { + Fail "Desktop update feed must be an absolute HTTPS URL when an update is advertised." + } + + if (-not $downloadUri.AbsolutePath.EndsWith(".msi", [System.StringComparison]::OrdinalIgnoreCase)) { + Fail "Desktop Windows updater expects an MSI download URL." + } +} + +Write-Host "QSfera Desktop update feed validation passed: $Url" diff --git a/Desktop/scripts/validate-production-release.ps1 b/Desktop/scripts/validate-production-release.ps1 index 92369d33..63acfe9c 100644 --- a/Desktop/scripts/validate-production-release.ps1 +++ b/Desktop/scripts/validate-production-release.ps1 @@ -1,5 +1,6 @@ param( [switch]$RequireSigning, + [switch]$VerifyUpdateFeed, [string]$Root = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path ) @@ -35,6 +36,10 @@ if ($branding -notmatch 'APPLICATION_DEFAULT_SERVER_URL\s+"https://qsfera\.kusof Fail "Desktop default server URL must be https://qsfera.kusoft.xyz." } +if ($branding -notmatch 'APPLICATION_UPDATE_URL\s+"https://qsfera\.kusoft\.xyz/desktop/updates/stable\.xml"') { + Fail "Desktop update feed URL must be https://qsfera.kusoft.xyz/desktop/updates/stable.xml." +} + if ($cmake -notmatch 'option\(WITH_UPDATE_NOTIFICATION\s+"Whether to check for new releases"\s+ON\)') { Fail "Desktop release builds must keep update notifications enabled by default." } @@ -55,6 +60,17 @@ if ($RequireSigning) { if ([string]::IsNullOrWhiteSpace($env:CRAFT_CODESIGN_CERTIFICATE)) { Fail "Production Windows tag releases require CRAFT_CODESIGN_CERTIFICATE." } + + $VerifyUpdateFeed = $true +} + +if ($VerifyUpdateFeed) { + $feedValidator = Join-Path $PSScriptRoot "validate-desktop-update-feed.ps1" + if (-not (Test-Path -LiteralPath $feedValidator)) { + Fail "Desktop update feed validator was not found: $feedValidator" + } + + & $feedValidator -Url "https://qsfera.kusoft.xyz/desktop/updates/stable.xml" } Write-Host "QSfera Desktop production release validation passed." diff --git a/PRODUCTION_READINESS.md b/PRODUCTION_READINESS.md index 49534b97..58020db5 100644 --- a/PRODUCTION_READINESS.md +++ b/PRODUCTION_READINESS.md @@ -36,6 +36,7 @@ Date: 2026-06-08. - Server Docker builds now accept explicit production metadata for `QSFERA_VERSION`, `QSFERA_EDITION`, and `QSFERA_BUILD_DATE`, and the Raspberry Pi 5 compose path defaults to pinned image `qsfera-cloud-server:7.0.0-rpi5-stable`. - 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`. ## Raspberry Pi 5 Check @@ -65,7 +66,7 @@ Date: 2026-06-08. - Fill production domains, ACME email, admin passwords, Keycloak passwords, and storage secrets from a secret manager. Do not commit real values. - Run `./validate-production-env.sh .env` and then `docker compose config` for `Server/devtools/deployments/qsfera_full` with a filled production `.env`; Docker CLI is not available in this local environment, so compose rendering is not locally verified. - Provide real Windows signing secrets in CI: `QSFERA_DESKTOP_SIGN_PACKAGE=True` and `QSFERA_DESKTOP_CODESIGN_CERTIFICATE` with the Craft-compatible signing certificate reference. -- Configure and verify the desktop updater endpoint via `APPLICATION_UPDATE_URL`; `Desktop/scripts/validate-production-release.ps1` confirms update notifications are enabled but cannot prove a live update feed without that URL. +- Publish the first signed Windows MSI and update `https://qsfera.kusoft.xyz/desktop/updates/stable.xml` from the current no-update XML to a concrete ``, ``, and HTTPS `.msi` ``. ## Remaining P1