Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,61 @@ jobs:
retention-days: 3
if-no-files-found: error

- name: Pack the same app for the Microsoft Store
# The identity Partner Center reserved; repository secrets, as the
# display name is a person's name and a run's log is public; secrets
# are masked there (desktop/msix.py, docs/desktop.md).
env:
MSIX_IDENTITY_NAME: ${{ secrets.MSIX_IDENTITY_NAME }}
MSIX_PUBLISHER: ${{ secrets.MSIX_PUBLISHER }}
MSIX_PUBLISHER_DISPLAY_NAME: ${{ secrets.MSIX_PUBLISHER_DISPLAY_NAME }}
run: ./scripts/build-msix.sh

- name: Install the Store package and check it on an empty administration
# Windows installs only a signed package, so a copy gets a throwaway
# certificate with the Store's publisher, trusted on this runner only;
# the Store signs the unsigned one itself. Started through its alias
# it runs inside the package, so the check also sees that it knows
# (the log line) and that its log reaches the real %LOCALAPPDATA%,
# not a folder of the package's own (FileSystemWriteVirtualization).
# Windows PowerShell: PowerShell 7 can't load the Appx module.
shell: powershell
env:
MSIX_PUBLISHER: ${{ secrets.MSIX_PUBLISHER }}
run: |
$ErrorActionPreference = 'Stop'
$sdk = (Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\10.*\x64" | Sort-Object { [version]$_.Parent.Name } | Select-Object -Last 1).FullName
$cert = New-SelfSignedCertificate -Type Custom -Subject $env:MSIX_PUBLISHER -KeyUsage DigitalSignature `
-CertStoreLocation Cert:\CurrentUser\My `
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}')
$password = ConvertTo-SecureString -String ([guid]::NewGuid()) -Force -AsPlainText
Export-PfxCertificate -Cert $cert -FilePath "$env:RUNNER_TEMP\check.pfx" -Password $password | Out-Null
Export-Certificate -Cert $cert -FilePath "$env:RUNNER_TEMP\check.cer" | Out-Null
Import-Certificate -FilePath "$env:RUNNER_TEMP\check.cer" -CertStoreLocation Cert:\LocalMachine\TrustedPeople | Out-Null
$signed = "$env:RUNNER_TEMP\Huismus-check.msix"
Copy-Item "dist\Huismus-$env:version-windows.msix" $signed
$plain = [Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password))
& "$sdk\signtool.exe" sign /fd SHA256 /f "$env:RUNNER_TEMP\check.pfx" /p $plain $signed
if ($LASTEXITCODE) { exit 1 }
Add-AppxPackage -Path $signed
$log = "$env:LOCALAPPDATA\Huismus\huismus.log"
Remove-Item $log -ErrorAction SilentlyContinue
$env:HUISMUS_DATA_DIR = "$env:RUNNER_TEMP\huismus-store-check"
$run = Start-Process -FilePath "$env:LOCALAPPDATA\Microsoft\WindowsApps\Huismus.exe" -ArgumentList '--check' -Wait -PassThru
if ($run.ExitCode -ne 0 -or -not (Select-String -Path $log -Pattern 'Microsoft Store' -Quiet)) {
Get-Content $log -ErrorAction SilentlyContinue
exit 1
}

- uses: actions/upload-artifact@v4
# For the submission in Partner Center (docs/desktop.md). Not named
# *-app: the publish job leaves it off the GitHub release.
with:
name: windows-store
path: dist/Huismus-${{ env.version }}-windows.msix
retention-days: 30
if-no-files-found: error

macos-window-test:
# The app as a user gets it, with its window (SPEC.md M11, "Release test
# in the window"; scripts/window-test-mac.sh): installed from the .dmg,
Expand Down
1 change: 1 addition & 0 deletions SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,7 @@ Beside the Docker/Pi route, not replacing it: Huismus as an ordinary desktop app
- **First start is the onboarding**: business details, then importing the Moneybird export (section 1) or skipping it, then the dashboard with its checklist (M8). Setting a password is offered there too: the done page of the step-by-step start (M13) links to Wachtwoord, in the desktop app only.
- **Signing.** Windows: signed for free through SignPath Foundation (decided 2026-11-12; until then not signed, as decided 2026-10-27), which signs open source projects with a certificate in its own name and asks for a code signing policy on the site (Meebouwen › Ondertekening, linked from the foot of every page) and a manual approval of every release by the maintainer. Signing the installer is enough: SmartScreen only checks the downloaded file. The workflow follows once SignPath has accepted the project and set it up. macOS: signed with an Apple Developer ID and notarized by Apple (decided 2026-11-12, the owner pays the € 99 a year), because the unsigned app needs a detour through System Settings that many users give up on. `release.yml` signs and notarizes once the owner has run `scripts/mac-ondertekenen.sh` (`docs/desktop.md`, "Ondertekenen op de Mac"); until then the macOS app carries only a free ad-hoc signature, without which it will not start on Apple Silicon. The download page explains, in Dutch and English, the one-time step on first opening: on macOS System Settings → Privacy & Security → "Open Anyway" (the step links straight to that screen), on Windows "More info" → "Run anyway". After the first notarized release the macOS step goes away.
- **Updates.** The app checks GitHub Releases for a new version, downloads it in the background and installs it by itself the next time it opens, before the user can work (the owner's decision, 2026-11-04; `huismus-intern/besluiten.md`); a button in Settings → Updates updates right away, and "Nu zoeken" there looks for a new version right away instead of waiting for the daily look (on Docker too; the owner's request, 2026-11-05). A version that fails to install automatically is tried once; after that only the button tries it again. The check only fetches the public release list and can be turned off in Settings. Before an update is applied the app makes a backup (`manage.py backup`, M7); the migrations run after. If they fail, the app says so and offers to restore that backup. After a new version, in the app and on Docker alike, the overview shows a blue notice "Huismus is bijgewerkt naar <version>." with a link to Hulp → Wat is er nieuw and a close button, until it is closed or that page is opened; the last version seen is kept in the update status file next to the database, not in a backup. A new administration and the sample data get no notice; an administration from before the notice gets it once (issue #149; `huismus-intern/besluiten.md`, 2026-11-12). Built 2026-10-29 for macOS (choices: `huismus-intern/besluiten.md`; how it works: `docs/desktop.md`): a release offers `Huismus-<version>-macos.dmg`, which `release.yml` builds on a version tag on an Apple Silicon runner and checks with `--check` before it is published (the tag must equal the version in `pyproject.toml`; the app needs macOS 15 or later, the Homebrew Pango it carries is built for that); the download must match the SHA-256 GitHub lists for it; the app replaces itself through a small script after it has closed. Built 2026-10-29 for Windows (choices: `huismus-intern/besluiten.md`, "Windows-app: bouwkeuzes"): a release offers `Huismus-<version>-windows.exe` as well, an Inno Setup installer that installs per user in `%LOCALAPPDATA%\Programs\Huismus` without asking for administrator rights; on Windows the app looks only for that file, and updating runs it without any window from a small PowerShell script after the app has closed. `release.yml` publishes a release only once both apps are built and checked (on Windows the installed app) and the release test in the window passed on both; a trial build by hand (workflow_dispatch) builds and tests both apps with a version one patch above the latest release, and publishes nothing (2026-11-13; before, it built only Windows). Windows 10 or later, x64 only; WebView2 is not installed along: when it is missing, the app asks in a Windows message whether to download it from Microsoft, then closes. Opened a second time on Windows, the app brings the open window forward. The app carries its own root certificates (certifi) for HTTPS, so the update check and "Hoe gebruik je Huismus?" also work on a Mac without Homebrew; `--check` checks that (2026-11-02).
- **Microsoft Store** (the owner registered a free developer account and reserved the name on 2026-11-13; choices: `huismus-intern/besluiten.md`, 2026-11-13; how it works: `docs/desktop.md`, "Microsoft Store"). The same `Huismus.exe` also goes to the Microsoft Store as an MSIX package, which Microsoft signs, beside the installer on GitHub Releases. `release.yml` builds it with the Windows app (`scripts/build-msix.sh`, manifest from `desktop/msix.py`, the reserved identity from repository secrets), installs a copy signed with a throwaway certificate and runs `--check` inside the package; the package is kept with the run, not on the release, and submitted by hand in Partner Center. The first submission is the owner's to make or approve. The app knows it runs from the package and leaves updating to the Store: it never looks at GitHub, downloads or installs a version, ignores and clears what the installer left in the update status, and Settings → Updates says only "De Microsoft Store werkt Huismus bij." (card without on/off). When a new version from the Store brings migrations an existing administration lacks, the app makes a backup before them, as before its own updates, and failing migrations offer it back. The manifest turns off file system virtualization (`unvirtualizedResources`), so the administration stays in `%APPDATA%\Huismus` for both versions and uninstalling keeps it; Windows 10 2004 or later. Help, download page and "Wat is er nieuw" follow once the Store page is live.
- **Backups.** Once a day, the app writes a backup archive to a folder the user picks once (iCloud Drive, OneDrive, Dropbox, a USB drive). The live database never goes into a synced folder, only the archives. The dashboard shows when the last backup was, and warns after a failed backup (folder unreachable or full) and after 7 days without one. Restoring is a button in the app, on top of `manage.py restore`: it first backs up the current state, and the app serves no pages while it restores. Built 2026-10-29 (choices: `huismus-intern/besluiten.md`; how it works: `docs/desktop.md`): the folder and the status belong to the computer and are not in a backup; a backup from a newer version is refused. Since M15 step 2 a backup can also go, encrypted, straight to Dropbox or WebDAV.
- **Release test in the window** (proposed 2026-11-07; the owner decided the same day to build it, and a failing test stops the release; issue #117). `--check` starts everything except the window, from the built app. A further job in `release.yml` tests what a user gets, on the same GitHub runners (a fresh virtual machine per run, nothing to rent). macOS: install from the `.dmg` as a user would. Windows: the app the installer put in place. Then open the app with its window on an empty administration, walk through the onboarding, make and issue one invoice, download its PDF, and keep a screenshot of each step with the release run. On Windows Playwright drives the app's own WebView2 window, with remote debugging switched on for the test only. On macOS Playwright cannot attach to a WKWebView, so it drives the same server while the test checks from a screenshot that the window opened. The test needs the window's token without that token reaching other programs outside the test. It also updates from the previous release's download and checks that the data stayed and a backup was made first. Together this replaces those steps of the check by hand in *Done when*. Built 2026-11-13 (choices: `huismus-intern/besluiten.md`; how it works: `docs/desktop.md`, "Een release maken"): jobs `macos-window-test` and `windows-window-test` run `scripts/window-test-mac.sh` and `scripts/window-test-windows.sh` with `scripts/window_test.py`, and `publish` needs both. The token: on macOS the test makes one and hands it to that one app process (`HUISMUS_RELEASE_TEST_TOKEN`, only one of 32 characters or more counts, taken out of the app's environment at once, masked in the run's log, never in a screenshot); on Windows the test needs none, and only its own environment switches remote debugging on, on 127.0.0.1 (`WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS`; the app never sets it). The update: the previous release, installed from its own download, gets records from its own `demo_data` (its source at that tag), and the new build is put in place as a version it downloaded and checked; it then installs it at its next start through its own path. Afterwards each kind and status of document, and the expenses, have the same count and total to the cent as before, so does the backup from before the update, and the overview says "Huismus is bijgewerkt naar <version>.". Not tested there: looking the release up at GitHub and downloading it (the release is not online yet); that, on one platform, stays a check by hand at each release. The warnings for an unsigned app (Gatekeeper, SmartScreen) only appear for a real download and stay a check by hand. No paid service: browser-testing services hardly test native desktop apps, and desktop test suites (Ranorex, TestComplete, Squish) are too heavy for a hobby project.
- **What it gives up.** The app can only be reached while the laptop is open: no access from a phone, so no PWA (M9). Decided by the owner on 2026-11-13 (`huismus-intern/besluiten.md`, issue #143): a later step links a phone over the user's own Wi-Fi with a QR code, never from the internet, and only after `review-veiligheid` has judged it. One installation per administration (invariant 2): moving between laptop and Pi goes through backup and restore, never two running copies of the same data.
Expand Down
7 changes: 7 additions & 0 deletions desktop/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,13 @@ def _configure_django(data):
os.environ["HUISMUS_DESKTOP"] = "true"
os.environ["HUISMUS_DATA_DIR"] = str(data)
os.environ["HUISMUS_WINDOW_TOKEN"] = _window_token()
if sys.platform == "win32":
from desktop import windows

if windows.packaged():
# The Microsoft Store version: the Store updates it (SPEC.md M11).
os.environ["HUISMUS_STORE"] = "true"
logger.info("Huismus uit de Microsoft Store: bijwerken gaat via de Store")
if getattr(sys, "frozen", False):
# Fontconfig (for WeasyPrint's PDFs) looks for its configuration
# where Homebrew or MSYS2 put it on the build machine; the app
Expand Down
Loading
Loading