From a5b87c5f24143444e5c06e1a6ce2869d79cb4691 Mon Sep 17 00:00:00 2001 From: bubakbubak500 Date: Thu, 23 Jul 2026 13:30:06 +0200 Subject: [PATCH] Prepare Windows release 0.35.0 --- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/workflows/release.yml | 67 ++++-------- README.md | 26 +++-- SECURITY.md | 8 +- docs/RELEASE_NOTES_0.35.0.md | 23 ++++ docs/RELEASING.md | 25 ++--- docs/USER_GUIDE.md | 9 +- installer/AntennaPatternLab.iss | 8 +- pyproject.toml | 2 +- src/antenna_pattern_lab/__init__.py | 2 +- src/antenna_pattern_lab/app.py | 14 ++- src/antenna_pattern_lab/ui.py | 13 +-- src/antenna_pattern_lab/update_dialog.py | 127 ++++++++++++++++------- tests/test_installer_definition.py | 2 + tests/test_update_dialog.py | 17 ++- tests/test_updates.py | 40 +++++++ 16 files changed, 244 insertions(+), 141 deletions(-) create mode 100644 docs/RELEASE_NOTES_0.35.0.md diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 6654d26..f656c54 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -10,7 +10,7 @@ body: id: version attributes: label: Application version - placeholder: "0.34.0" + placeholder: "0.35.0" validations: required: true - type: dropdown diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 08b1593..e053aa2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,4 +1,4 @@ -name: Signed Windows release +name: Windows release on: push: @@ -7,6 +7,8 @@ on: permissions: contents: write + id-token: write + attestations: write jobs: release: @@ -44,70 +46,31 @@ jobs: - name: Build application run: pyinstaller --noconfirm AntennaPatternLab.spec - - name: Import code-signing certificate - id: certificate - shell: pwsh - env: - CERTIFICATE_BASE64: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_BASE64 }} - CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_SIGNING_CERTIFICATE_PASSWORD }} - run: | - if (-not $env:CERTIFICATE_BASE64 -or -not $env:CERTIFICATE_PASSWORD) { - throw "Code-signing secrets are not configured." - } - $pfx = Join-Path $env:RUNNER_TEMP "code-signing.pfx" - [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:CERTIFICATE_BASE64)) - $password = ConvertTo-SecureString $env:CERTIFICATE_PASSWORD -AsPlainText -Force - $certificate = Import-PfxCertificate -FilePath $pfx -CertStoreLocation Cert:\CurrentUser\My -Password $password | - Where-Object HasPrivateKey | - Select-Object -First 1 - if (-not $certificate.Thumbprint) { throw "Certificate import failed." } - "thumbprint=$($certificate.Thumbprint)" >> $env:GITHUB_OUTPUT - Remove-Item -LiteralPath $pfx -Force - - - name: Locate signing and installer tools + - name: Install Inno Setup id: tools shell: pwsh run: | choco install innosetup --no-progress -y - $signtool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" | - Sort-Object FullName -Descending | - Select-Object -First 1 $iscc = Get-Item "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe" - if (-not $signtool -or -not $iscc) { throw "Required build tools were not found." } - "signtool=$($signtool.FullName)" >> $env:GITHUB_OUTPUT + if (-not $iscc) { throw "Inno Setup was not found." } "iscc=$($iscc.FullName)" >> $env:GITHUB_OUTPUT - - name: Sign application - shell: pwsh - run: | - & "${{ steps.tools.outputs.signtool }}" sign ` - /fd SHA256 ` - /sha1 "${{ steps.certificate.outputs.thumbprint }}" ` - /td SHA256 ` - /tr http://timestamp.digicert.com ` - "dist\AntennaPatternLab\AntennaPatternLab.exe" - if ($LASTEXITCODE -ne 0) { throw "Application signing failed." } - - - name: Build signed installer and update manifest + - name: Build installer and update manifest shell: pwsh run: | .\build_installer.ps1 ` -Compiler "${{ steps.tools.outputs.iscc }}" ` - -SignTool "${{ steps.tools.outputs.signtool }}" ` - -CertificateThumbprint "${{ steps.certificate.outputs.thumbprint }}" ` -ReleaseBaseUrl "https://github.com/bubakbubak500/AntennaPatternLab/releases/latest/download" - - name: Verify signatures and package application + - name: Package application and publish checksums shell: pwsh run: | $project = Get-Content pyproject.toml -Raw $version = [regex]::Match($project, '(?m)^version\s*=\s*"([^"]+)"').Groups[1].Value $installer = "release\AntennaPatternLab-$version-setup-win-x64.exe" - foreach ($file in @("dist\AntennaPatternLab\AntennaPatternLab.exe", $installer)) { - $signature = Get-AuthenticodeSignature -FilePath $file - if ($signature.Status -ne "Valid") { - throw "Invalid Authenticode signature for $file`: $($signature.Status)" - } + $signature = Get-AuthenticodeSignature -FilePath $installer + if ($signature.Status -ne "NotSigned") { + throw "Unexpected installer signing state: $($signature.Status)" } Compress-Archive -Path "dist\AntennaPatternLab\*" ` -DestinationPath "release\AntennaPatternLab-$version-win-x64.zip" @@ -116,6 +79,13 @@ jobs: ForEach-Object { "$($_.Hash.ToLowerInvariant()) $([IO.Path]::GetFileName($_.Path))" } | Set-Content release\SHA256SUMS.txt -Encoding ascii + - name: Attest release build provenance + uses: actions/attest@v4 + with: + subject-path: | + release/AntennaPatternLab-*-setup-win-x64.exe + release/AntennaPatternLab-*-win-x64.zip + - name: Publish GitHub Release shell: pwsh env: @@ -129,5 +99,6 @@ jobs: "release\release-manifest.json" ` "release\SHA256SUMS.txt" ` --title "Antenna Pattern Lab $version" ` - --generate-notes ` + --notes-file "docs\RELEASE_NOTES_0.35.0.md" ` + --latest ` --verify-tag diff --git a/README.md b/README.md index ad9ac51..fcb9274 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,20 @@ without requiring a server. ## Download -Download the current signed Windows installer from +Download the current Windows installer from [GitHub Releases](https://github.com/bubakbubak500/AntennaPatternLab/releases/latest). The application can check the same official GitHub release channel for updates. -Automatic checks are opt-in. Every downloaded installer is accepted only after its -SHA-256 digest matches the published release manifest, and it is never launched -without an additional confirmation. +It checks in the background on every startup and fails silently when offline. +Every downloaded installer is accepted only after its SHA-256 digest matches the +published release manifest, and it is never launched without an additional +confirmation. + +> [!WARNING] +> Current Windows packages are not Authenticode-signed. Windows can therefore +> display **Unknown publisher** or a Microsoft Defender SmartScreen warning. +> Download only from this repository's Releases page. Each release includes +> SHA-256 checksums and GitHub build-provenance attestations. ## Main features @@ -38,13 +45,14 @@ without an additional confirmation. - local SQLite storage and diagnostic export; - English and Czech application UI; - verified, consent-driven setup assistance for WSJT-X and Hamlib; -- signed Windows application and installer releases. +- reproducible Windows releases with checksums and GitHub build provenance. ## Quick start 1. Download the latest installer from [Releases](https://github.com/bubakbubak500/AntennaPatternLab/releases/latest). -2. Verify that Windows reports the expected publisher before installing. +2. Compare the installer's SHA-256 with `SHA256SUMS.txt`; Windows currently + reports an unknown publisher because the release is unsigned. 3. Start Antenna Pattern Lab and complete the first-run assistant. 4. Use **Help → Add demo data** to explore the application without a radio. 5. For live collection, enter your callsign and locator, select the band and mode, @@ -99,9 +107,9 @@ Build an unsigned local installer: .\build_installer.ps1 ``` -Official releases are built and Authenticode-signed by GitHub Actions. Signing -credentials are stored only as encrypted GitHub Actions secrets and are never -committed to this repository. See [Release process](docs/RELEASING.md). +Official releases are built by GitHub Actions, accompanied by SHA-256 checksums +and a GitHub artifact attestation. Authenticode signing can be added later +without changing the update channel. See [Release process](docs/RELEASING.md). ## Contributing diff --git a/SECURITY.md b/SECURITY.md index aa0105c..a6866d1 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -22,6 +22,8 @@ mitigation. You should receive an initial response within seven days. ## Release integrity Official Windows releases are published only through GitHub Releases. The -application and installer are Authenticode-signed, release checksums are -published, and the in-app updater verifies the installer's SHA-256 before making -it available to launch. +current application and installer are not Authenticode-signed. Release checksums +and GitHub build-provenance attestations are published, and the in-app updater +verifies the installer's SHA-256 before making it available to launch. Windows +may show an unknown-publisher warning until a trusted signing certificate is +introduced. diff --git a/docs/RELEASE_NOTES_0.35.0.md b/docs/RELEASE_NOTES_0.35.0.md new file mode 100644 index 0000000..a3864dd --- /dev/null +++ b/docs/RELEASE_NOTES_0.35.0.md @@ -0,0 +1,23 @@ +# Antenna Pattern Lab 0.35.0 + +This release establishes the public GitHub update channel and improves Windows +shell integration. + +## What changed + +- The application now checks the official GitHub Releases channel in the + background on every startup. Offline or temporary GitHub failures are silent. +- The Updates dialog now explains the download, SHA-256 verification, and + installation flow instead of exposing an editable manifest URL. +- The application process, Start menu shortcut, and desktop shortcut now share + the stable Windows AppUserModelID `OK7PS.AntennaPatternLab`. +- The installer deploys the application icon explicitly for Windows shortcuts. +- Release assets include `SHA256SUMS.txt`, an update manifest, and a GitHub + build-provenance attestation. + +## Windows signing notice + +The application and installer in this release are **not Authenticode-signed**. +Windows can display an **Unknown publisher** or Microsoft Defender SmartScreen +warning. Download only from the official AntennaPatternLab GitHub Releases page +and verify the SHA-256 checksum supplied with the release. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 016b1cf..055c0de 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -2,30 +2,25 @@ Official releases are created by `.github/workflows/release.yml`. -## Required repository secrets - -- `WINDOWS_SIGNING_CERTIFICATE_BASE64`: a Base64-encoded, password-protected PFX - code-signing certificate; -- `WINDOWS_SIGNING_CERTIFICATE_PASSWORD`: the PFX password. - -The certificate must support Windows code signing and chain to a certificate -authority trusted by supported Windows versions. Never commit the PFX or password. +No release secrets are currently required. Packages are intentionally published +without an Authenticode signature until a trusted code-signing certificate is +available. The release page and application clearly disclose this. ## Create a release 1. Update the version in `pyproject.toml`. 2. Ensure the Inno Setup definition and application package use the same version. 3. Merge the version change through an approved pull request. -4. Create and push a signed or annotated tag named `vMAJOR.MINOR.PATCH`. +4. Create and push a tag named `vMAJOR.MINOR.PATCH`. 5. The release workflow verifies that the tag matches the project version. -6. GitHub Actions runs tests, builds the application, signs the application EXE, - builds and signs the installer and uninstaller, verifies Authenticode, creates - the update manifest and checksums, and publishes a GitHub Release. +6. GitHub Actions runs tests, builds the application and installer, confirms that + the installer is unsigned, creates the update manifest and checksums, records + GitHub build-provenance attestations, and publishes a GitHub Release. The update manifest is always available at: `https://github.com/bubakbubak500/AntennaPatternLab/releases/latest/download/release-manifest.json` -If certificate renewal is required, replace the two secrets before creating a -release. Existing releases remain timestamp-valid after the certificate expires, -provided the timestamp was created while the certificate was valid. +When Authenticode signing is added later, keep the same asset names and manifest +URL so installed applications remain on the same update channel. Never commit a +PFX file or its password. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index ddd70d7..6dfbebf 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -99,8 +99,9 @@ CSV export can be used for independent analysis or archival. ## 9. Updates -Open **Help → Updates** to check the official GitHub release channel. Automatic -checks at startup are disabled until you opt in. +Open **Settings → Updates** to check the official GitHub release channel. Automatic +checks run in the background on every startup. If the internet or GitHub is +unavailable, startup continues normally and no error is shown. The update process: @@ -110,7 +111,9 @@ The update process: 4. publishes the `.exe` only after the digest matches; 5. asks before launching the verified installer. -Always confirm that Windows displays the expected Authenticode publisher. +Current releases are not Authenticode-signed, so Windows may report an unknown +publisher or show a SmartScreen warning. Download only from the official GitHub +Releases page and compare the installer with the published SHA-256 checksum. ## 10. Diagnostics and data diff --git a/installer/AntennaPatternLab.iss b/installer/AntennaPatternLab.iss index 7a392a8..8d4e07e 100644 --- a/installer/AntennaPatternLab.iss +++ b/installer/AntennaPatternLab.iss @@ -1,6 +1,6 @@ #define MyAppName "Antenna Pattern Lab" #ifndef MyAppVersion -#define MyAppVersion "0.34.0" +#define MyAppVersion "0.35.0" #endif #define MyAppPublisher "OK7PS" #define MyAppExeName "AntennaPatternLab.exe" @@ -28,6 +28,7 @@ SolidCompression=yes WizardStyle=modern dynamic SetupIconFile=..\src\antenna_pattern_lab\assets\app-icon.ico UninstallDisplayIcon={app}\{#MyAppExeName} +ChangesAssociations=yes CloseApplications=yes RestartApplications=no VersionInfoVersion={#MyAppVersion}.0 @@ -70,11 +71,12 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{ [Files] Source: "..\dist\AntennaPatternLab\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs +Source: "..\src\antenna_pattern_lab\assets\app-icon.ico"; DestDir: "{app}"; DestName: "AntennaPatternLab.ico"; Flags: ignoreversion [Icons] -Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\AntennaPatternLab.ico"; AppUserModelID: "OK7PS.AntennaPatternLab" Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; IconFilename: "{app}\AntennaPatternLab.ico"; AppUserModelID: "OK7PS.AntennaPatternLab"; Tasks: desktopicon [Run] Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent diff --git a/pyproject.toml b/pyproject.toml index 5e61444..b7b2b86 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "antenna-pattern-lab" -version = "0.34.0" +version = "0.35.0" description = "Empirical antenna coverage analysis from PSK Reporter FT8 spots" readme = "README.md" requires-python = ">=3.11" diff --git a/src/antenna_pattern_lab/__init__.py b/src/antenna_pattern_lab/__init__.py index 7b448a8..531b939 100644 --- a/src/antenna_pattern_lab/__init__.py +++ b/src/antenna_pattern_lab/__init__.py @@ -1,3 +1,3 @@ """Antenna Pattern Lab.""" -__version__ = "0.34.0" +__version__ = "0.35.0" diff --git a/src/antenna_pattern_lab/app.py b/src/antenna_pattern_lab/app.py index 3ab7c32..0b5a502 100644 --- a/src/antenna_pattern_lab/app.py +++ b/src/antenna_pattern_lab/app.py @@ -21,8 +21,20 @@ def application_icon_path() -> Path: return Path(__file__).resolve().parent / "assets" / "app-icon.png" +def set_windows_app_user_model_id() -> None: + """Give Windows one stable identity for the process and its shortcuts.""" + if sys.platform != "win32": + return + import ctypes + + ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID( + "OK7PS.AntennaPatternLab" + ) + + def main() -> int: logging.basicConfig(level=logging.INFO) + set_windows_app_user_model_id() application = QApplication(sys.argv) application.setApplicationName("Antenna Pattern Lab") application.setOrganizationName("OK7PS") @@ -43,7 +55,7 @@ def main() -> int: window = MainWindow(repository) window.show() QTimer.singleShot(0, window.show_setup_if_needed) - QTimer.singleShot(1500, window.check_updates_if_enabled) + QTimer.singleShot(1500, window.check_updates_at_startup) return application.exec() diff --git a/src/antenna_pattern_lab/ui.py b/src/antenna_pattern_lab/ui.py index a74d77e..afb2a42 100644 --- a/src/antenna_pattern_lab/ui.py +++ b/src/antenna_pattern_lab/ui.py @@ -663,6 +663,7 @@ def __init__(self, repository: SpotRepository, settings: QSettings | None = None saved_language = str(self.settings.value("language", "CZE")) self.language_code = saved_language if saved_language in TRANSLATIONS else "CZE" self.setWindowTitle("Antenna Pattern Lab · FT8 / WSPR") + self.setWindowIcon(QApplication.instance().windowIcon()) self.resize(1180, 760) self._build_ui() self._connect_signals() @@ -2403,18 +2404,10 @@ def show_setup_if_needed(self) -> None: def _open_updates(self) -> None: UpdateDialog(self.settings, self.language_code, self).exec() - def check_updates_if_enabled(self) -> None: - if not bool(int(self.settings.value("automatic_update_checks", 0))): - return - url = str( - self.settings.value("release_manifest_url", DEFAULT_RELEASE_MANIFEST_URL) - ).strip() - if not url: - return - + def check_updates_at_startup(self) -> None: def worker() -> None: try: - result = check_for_update(url, __version__) + result = check_for_update(DEFAULT_RELEASE_MANIFEST_URL, __version__) except Exception as exc: self.bridge.update_failed.emit(str(exc)) else: diff --git a/src/antenna_pattern_lab/update_dialog.py b/src/antenna_pattern_lab/update_dialog.py index defb370..9cf2dc1 100644 --- a/src/antenna_pattern_lab/update_dialog.py +++ b/src/antenna_pattern_lab/update_dialog.py @@ -3,11 +3,9 @@ from PySide6.QtCore import QSettings, QStandardPaths, QUrl from PySide6.QtGui import QDesktopServices from PySide6.QtWidgets import ( - QCheckBox, QDialog, QHBoxLayout, QLabel, - QLineEdit, QMessageBox, QPushButton, QVBoxLayout, @@ -25,35 +23,75 @@ TEXT = { "CZE": { "title": "Aktualizace aplikace", - "channel": "HTTPS adresa release manifestu", - "channel_help": "Manifest musí obsahovat version, installer_url a sha256. Bez oficiálního kanálu ponechte pole prázdné.", - "automatic": "Automaticky kontrolovat při spuštění (pouze opt-in)", - "check": "Zkontrolovat", + "heading": "Oficiální aktualizace přes GitHub Releases", + "channel_help": ( + "Antenna Pattern Lab při každém spuštění na pozadí zkontroluje " + "oficiální GitHub release kanál. Když není internet dostupný, " + "aplikace pokračuje bez upozornění." + ), + "process": ( + "Aktuální verze: {version}\n\n" + "Instalátor se stáhne do složky Stažené soubory a aplikace před " + "jeho nabídnutím ověří publikovaný kontrolní součet SHA-256. " + "Instalátor se nikdy nespustí bez vašeho potvrzení.\n\n" + "Toto vydání zatím není podepsané Authenticode certifikátem. " + "Windows proto může zobrazit varování „Neznámý vydavatel“. " + "Stahujte pouze z oficiálního GitHub repozitáře." + ), + "releases": ( + 'Otevřít GitHub Releases' + ), + "check": "Zkontrolovat nyní", "download": "Stáhnout ověřený instalátor…", "close": "Zavřít", - "missing": "Nejdříve zadejte HTTPS adresu oficiálního manifestu.", "current": "Používáte aktuální verzi {version}.", "available": "Je dostupná verze {version}.", "failed": "Kontrola aktualizace selhala: {error}", "confirm_title": "Stáhnout aktualizaci?", - "confirm": "Stáhnout instalátor verze {version} a ověřit jeho SHA-256? Nic se nespustí automaticky.", - "downloaded": "Ověřený instalátor byl uložen:\n{path}\n\nOtevřít jej nyní?", + "confirm": ( + "Stáhnout instalátor verze {version} a ověřit jeho SHA-256? " + "Nic se nespustí automaticky." + ), + "downloaded": ( + "Ověřený instalátor byl uložen:\n{path}\n\nOtevřít jej nyní?" + ), }, "ENG": { "title": "Application updates", - "channel": "HTTPS release manifest URL", - "channel_help": "Official GitHub Releases channel. The manifest must contain version, installer_url and sha256.", - "automatic": "Check automatically at startup (opt-in only)", - "check": "Check", + "heading": "Official updates through GitHub Releases", + "channel_help": ( + "Antenna Pattern Lab checks the official GitHub release channel " + "in the background on every startup. If the internet is unavailable, " + "the application continues silently." + ), + "process": ( + "Current version: {version}\n\n" + "The installer is downloaded to your Downloads folder. Before it " + "is offered, the application verifies its published SHA-256 checksum. " + "The installer is never launched without your confirmation.\n\n" + "This release is not yet signed with an Authenticode certificate, " + "so Windows may display an “Unknown publisher” warning. Download " + "only from the official GitHub repository." + ), + "releases": ( + 'Open GitHub Releases' + ), + "check": "Check now", "download": "Download verified installer…", "close": "Close", - "missing": "Enter the HTTPS URL of the official manifest first.", "current": "You are using the current version {version}.", "available": "Version {version} is available.", "failed": "Update check failed: {error}", "confirm_title": "Download update?", - "confirm": "Download installer version {version} and verify its SHA-256? Nothing will run automatically.", - "downloaded": "The verified installer was saved to:\n{path}\n\nOpen it now?", + "confirm": ( + "Download installer version {version} and verify its SHA-256? " + "Nothing will run automatically." + ), + "downloaded": ( + "The verified installer was saved to:\n{path}\n\nOpen it now?" + ), }, } @@ -65,23 +103,28 @@ def __init__(self, settings: QSettings, language: str = "CZE", parent=None): self.text = TEXT[language if language in TEXT else "CZE"] self.update_check: UpdateCheck | None = None self.setWindowTitle(self.text["title"]) - self.resize(650, 300) + self.resize(680, 390) + layout = QVBoxLayout(self) - layout.addWidget(QLabel(self.text["channel"])) - self.channel_url = QLineEdit( - str(settings.value("release_manifest_url", DEFAULT_RELEASE_MANIFEST_URL)) - ) - layout.addWidget(self.channel_url) + layout.addWidget(QLabel(f"

{self.text['heading']}

")) + help_label = QLabel(self.text["channel_help"]) help_label.setWordWrap(True) layout.addWidget(help_label) - self.automatic = QCheckBox(self.text["automatic"]) - self.automatic.setChecked(bool(int(settings.value("automatic_update_checks", 0)))) - layout.addWidget(self.automatic) + + process_label = QLabel(self.text["process"].format(version=__version__)) + process_label.setWordWrap(True) + layout.addWidget(process_label) + + releases_label = QLabel(self.text["releases"]) + releases_label.setOpenExternalLinks(True) + layout.addWidget(releases_label) + self.status = QLabel() self.status.setWordWrap(True) layout.addWidget(self.status) layout.addStretch() + buttons = QHBoxLayout() self.check_button = QPushButton(self.text["check"]) self.download_button = QPushButton(self.text["download"]) @@ -92,23 +135,24 @@ def __init__(self, settings: QSettings, language: str = "CZE", parent=None): buttons.addStretch() buttons.addWidget(close_button) layout.addLayout(buttons) + self.check_button.clicked.connect(self.check_now) self.download_button.clicked.connect(self.download_update) close_button.clicked.connect(self.accept) def save_settings(self) -> None: - self.settings.setValue("release_manifest_url", self.channel_url.text().strip()) - self.settings.setValue("automatic_update_checks", int(self.automatic.isChecked())) + # Migrate settings from releases before 0.35.0. The official channel is + # fixed and the lightweight startup check is now always enabled. + self.settings.remove("release_manifest_url") + self.settings.remove("automatic_update_checks") self.settings.sync() def check_now(self) -> None: self.save_settings() - url = self.channel_url.text().strip() - if not url: - self.status.setText(self.text["missing"]) - return try: - self.update_check = check_for_update(url, __version__) + self.update_check = check_for_update( + DEFAULT_RELEASE_MANIFEST_URL, __version__ + ) except Exception as exc: self.update_check = None self.download_button.setEnabled(False) @@ -116,27 +160,38 @@ def check_now(self) -> None: return self.download_button.setEnabled(self.update_check.update_available) key = "available" if self.update_check.update_available else "current" - self.status.setText(self.text[key].format(version=self.update_check.manifest.version if self.update_check.update_available else __version__)) + version = ( + self.update_check.manifest.version + if self.update_check.update_available + else __version__ + ) + self.status.setText(self.text[key].format(version=version)) def download_update(self) -> None: if self.update_check is None or not self.update_check.update_available: return manifest = self.update_check.manifest answer = QMessageBox.question( - self, self.text["confirm_title"], self.text["confirm"].format(version=manifest.version), + self, + self.text["confirm_title"], + self.text["confirm"].format(version=manifest.version), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) if answer != QMessageBox.StandardButton.Yes: return - downloads = QStandardPaths.writableLocation(QStandardPaths.StandardLocation.DownloadLocation) + downloads = QStandardPaths.writableLocation( + QStandardPaths.StandardLocation.DownloadLocation + ) try: path = download_verified_installer(manifest, downloads) except Exception as exc: self.status.setText(self.text["failed"].format(error=exc)) return open_answer = QMessageBox.question( - self, self.text["confirm_title"], self.text["downloaded"].format(path=path), + self, + self.text["confirm_title"], + self.text["downloaded"].format(path=path), QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, QMessageBox.StandardButton.No, ) diff --git a/tests/test_installer_definition.py b/tests/test_installer_definition.py index 5517b98..173eacc 100644 --- a/tests/test_installer_definition.py +++ b/tests/test_installer_definition.py @@ -43,3 +43,5 @@ def test_installer_uses_application_icon_and_fills_dependency_memo(): spec = (ROOT / "AntennaPatternLab.spec").read_text(encoding="utf-8") assert 'icon="src/antenna_pattern_lab/assets/app-icon.ico"' in spec assert "assets/app-icon.png" in spec + assert 'AppUserModelID: "OK7PS.AntennaPatternLab"' in script + assert 'IconFilename: "{app}\\AntennaPatternLab.ico"' in script diff --git a/tests/test_update_dialog.py b/tests/test_update_dialog.py index a9645d5..8b32a3f 100644 --- a/tests/test_update_dialog.py +++ b/tests/test_update_dialog.py @@ -6,18 +6,15 @@ from PySide6.QtWidgets import QApplication from antenna_pattern_lab.update_dialog import UpdateDialog -from antenna_pattern_lab.updates import DEFAULT_RELEASE_MANIFEST_URL - - -def test_update_dialog_is_opt_in_and_persists_channel(tmp_path): +def test_update_dialog_uses_fixed_channel_and_migrates_old_settings(tmp_path): application = QApplication.instance() or QApplication([]) settings = QSettings(str(tmp_path / "updates.ini"), QSettings.Format.IniFormat) + settings.setValue("release_manifest_url", "https://releases.example/manifest.json") + settings.setValue("automatic_update_checks", 0) dialog = UpdateDialog(settings, "ENG") - assert not dialog.automatic.isChecked() - assert dialog.channel_url.text() == DEFAULT_RELEASE_MANIFEST_URL - dialog.channel_url.setText("https://releases.example/manifest.json") - dialog.automatic.setChecked(True) + assert not hasattr(dialog, "channel_url") + assert not hasattr(dialog, "automatic") dialog.accept() - assert settings.value("release_manifest_url") == "https://releases.example/manifest.json" - assert int(settings.value("automatic_update_checks")) == 1 + assert not settings.contains("release_manifest_url") + assert not settings.contains("automatic_update_checks") application.processEvents() diff --git a/tests/test_updates.py b/tests/test_updates.py index 327b294..9e3e4ad 100644 --- a/tests/test_updates.py +++ b/tests/test_updates.py @@ -1,6 +1,7 @@ from io import BytesIO import hashlib import json +from types import SimpleNamespace import pytest @@ -9,6 +10,7 @@ download_verified_installer, parse_release_manifest, ) +from antenna_pattern_lab import ui class Response(BytesIO): @@ -19,6 +21,22 @@ def __exit__(self, *_args): self.close() +class ImmediateThread: + def __init__(self, *, target, **_kwargs): + self.target = target + + def start(self): + self.target() + + +class RecordedSignal: + def __init__(self): + self.values = [] + + def emit(self, value): + self.values.append(value) + + def test_default_release_channel_is_official_github_latest_release(): assert DEFAULT_RELEASE_MANIFEST_URL == ( "https://github.com/bubakbubak500/AntennaPatternLab/" @@ -26,6 +44,28 @@ def test_default_release_channel_is_official_github_latest_release(): ) +def test_startup_always_checks_the_fixed_release_channel(monkeypatch): + expected = object() + requested = [] + monkeypatch.setattr(ui.threading, "Thread", ImmediateThread) + monkeypatch.setattr( + ui, + "check_for_update", + lambda url, version: requested.append((url, version)) or expected, + ) + checked = RecordedSignal() + failed = RecordedSignal() + window = SimpleNamespace( + bridge=SimpleNamespace(update_checked=checked, update_failed=failed) + ) + + ui.MainWindow.check_updates_at_startup(window) + + assert requested == [(DEFAULT_RELEASE_MANIFEST_URL, ui.__version__)] + assert checked.values == [expected] + assert failed.values == [] + + def test_manifest_requires_https_hash_and_newer_semver(): payload = json.dumps( {