From 9fa84cce4f5402a16085882a0db742eded46a689 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Wed, 5 Aug 2026 14:22:48 +0200 Subject: [PATCH 1/3] docs(dist): make the Microsoft Store the recommended Windows route MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both Windows artifacts leave CI unsigned — verified with Get-AuthenticodeSignature on the 1.8.0 build, the .exe and the .appx alike. Microsoft signs Store submissions during certification and that signed copy lives only in the Store, so it cannot be redistributed. That makes the Store the only Windows route that installs without a SmartScreen interstitial, and it costs nothing beyond the developer account already paid for. The README now recommends it first, with winget --source msstore as the terminal equivalent, and keeps the .exe as an explicitly documented fallback for people who cannot reach the Store. The fallback carries an explanation rather than an apology: reputation attaches to the file hash while an installer is unsigned, so every release starts untrusted however popular the last one was, and there is no signature for a cautious user to check. Saying so is more useful than letting people guess whether the download is safe. Also records two things that were true but written down nowhere: that no Windows signing exists in the repo at all, and that publish-winget.yml has reported "skipped" on every release because WINGET_IDENTIFIER was never set. Turning it on would publish the unsigned .exe to the community source alongside the signed Store package, which is a decision rather than a variable, so the note says as much instead of just listing the secret. Buying a certificate stays open and is not foreclosed here; it now smooths a secondary path rather than the only one. --- README.md | 21 +++++++++++++++++-- .../engineering/build-and-packaging.md | 15 +++++++++++++ .../engineering/release-and-secrets.md | 4 ++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 20c8dc4ea..59ea36cfc 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ See [docs/cli.md](./docs/cli.md). ## Installation -Download the latest installer for your platform from the [GitHub Releases](https://github.com/getopenscreen/openscreen/releases) page. +Every platform has a recommended route below. On Windows that is the Microsoft Store; everywhere else it is the installer from the [GitHub Releases](https://github.com/getopenscreen/openscreen/releases) page. ### macOS @@ -79,7 +79,24 @@ After running this command, proceed to **System Settings > Privacy & Security** ### Windows -Download the `.exe` installer directly from the [Releases page](https://github.com/getopenscreen/openscreen/releases). +**Recommended — Microsoft Store** + +[Get OpenScreen from the Microsoft Store](https://apps.microsoft.com/detail/9MXQ1HQJL5G5), or from a terminal: + +```powershell +winget install --source msstore OpenScreen +``` + +Microsoft signs the Store package during certification, so it installs with no security warning and updates itself. + +**Alternative — standalone installer** + +Download the `.exe` from the [Releases page](https://github.com/getopenscreen/openscreen/releases). Use this if you can't reach the Store — Windows LTSC, a locked-down work machine, an offline install, or if you want a specific older version. + +> [!NOTE] +> The `.exe` is not code-signed, so Windows SmartScreen shows **"Windows protected your PC"** and reports an unknown publisher. Choose **More info** → **Run anyway** to continue. +> +> This is not a sign that something is wrong with the download: an unsigned installer earns SmartScreen's trust per file, so a brand-new build always starts out untrusted no matter how many people installed the previous one. Verifying the signature isn't an option here — there is nothing to verify. If you want the checked path, use the Store build above. If you use the `.exe`, download it only from the Releases page linked here. ### Linux diff --git a/technical-documentation/engineering/build-and-packaging.md b/technical-documentation/engineering/build-and-packaging.md index d7fbdd3c4..cedba54a2 100644 --- a/technical-documentation/engineering/build-and-packaging.md +++ b/technical-documentation/engineering/build-and-packaging.md @@ -207,6 +207,21 @@ The default electron-builder target is NSIS, with an assisted installer that all `build/appx/` is therefore committed, and `npm run assets:appx` (`scripts/generate-appx-assets.mjs`) regenerates it from `icons/icons/png/1024x1024.png` — **run it whenever the app icon changes**. No build step calls it, because generated-at-build assets would go missing exactly when someone packages from a checkout that skipped the step. The script decodes and writes PNGs directly on `node:zlib` rather than pulling an image library into the dependency tree for seven static logos. Four of the file names are load-bearing: `StoreLogo`, `Square150x150Logo`, `Square44x44Logo` and `Wide310x150Logo` are the ones electron-builder replaces with placeholders when absent. `SmallTile`, `LargeTile` and `SplashScreen` are opt-in — their `` manifest attributes are emitted only when a matching file is present. Each logo also ships `.scale-125/150/200` variants (plus `.scale-400` on the small assets) and, for the 44x44 app-list icon, `.targetsize-*` and `.targetsize-*_altform-unplated` variants for the taskbar and Start list. Any `.scale-`/`.targetsize-` file switches electron-builder into its `makepri.exe` path, which generates `resources.pri` and packages it alongside; the unqualified 100% file of every asset is kept as the neutral fallback candidate so an unresolved qualifier still finds art. Because `appx.backgroundColor` is `transparent`, Windows paints the tile in the user's accent colour, so the tiles are drawn as a padded logo on a transparent canvas rather than full-bleed art — and the two tiles carrying `showNameOnTiles` shift their logo up to clear the name band. +#### Neither Windows artifact is signed + +Unlike macOS, no Windows signing is configured anywhere in the repo. Both CI artifacts come out unsigned — confirmed by `Get-AuthenticodeSignature` on the 1.8.0 build: + +| Artifact | Signature | +|---|---| +| `Openscreen.Setup.1.8.0.exe` | `NotSigned` | +| `Openscreen.Setup.1.8.0.appx` | `NotSigned` | + +That the AppX is unsigned is not a defect: Microsoft signs Store submissions during certification, and the signed copy exists only in the Store. It is never handed back, so it cannot be redistributed. Two consequences worth knowing before anyone tries to "just ship the appx instead": + +- **The AppX is not a drop-in replacement for the NSIS installer.** Windows runs an unsigned `.exe` after a SmartScreen prompt, but refuses outright to install an unsigned MSIX/AppX — sideloading requires a signature the machine already trusts. Swapping one for the other makes distribution strictly worse. +- **SmartScreen reputation is per file hash while the installer is unsigned**, so every release starts from zero and users meet the interstitial again on each new version. Signing would attach reputation to the publisher identity instead, and it would accumulate across releases. + +Buying a certificate is the fix for the `.exe`, and it stays a live option (roughly €120/year for a cloud-HSM certificate an individual can buy, since the 2023 baseline requirements forbid keeping the key in a file). It was deliberately deferred: the Store route is already signed and already paid for through the developer account, so the README recommends it first and treats the `.exe` as the documented fallback. ### macOS diff --git a/technical-documentation/engineering/release-and-secrets.md b/technical-documentation/engineering/release-and-secrets.md index 67b3cb65e..c76712209 100644 --- a/technical-documentation/engineering/release-and-secrets.md +++ b/technical-documentation/engineering/release-and-secrets.md @@ -147,6 +147,10 @@ The bot token comes from a Discord application authorized with the `bot` scope. `bump-nix-package.yml` uses the workflow-scoped `GITHUB_TOKEN`; it requires repository contents and pull-request write permissions as declared in the workflow and has no additional long-lived secret. +**WinGet publishing is built but switched off.** `publish-winget.yml` gates on `vars.WINGET_IDENTIFIER != ''`, and neither that variable nor `WINGET_ACC_TOKEN` is set, so the job has reported `skipped` on every release so far — including stable ones. Nothing is broken; it has simply never run. Setting both turns it on for the next stable tag with no code change. + +Note what it would publish before turning it on: `winget-releaser` submits the **NSIS `.exe`** attached to the release to the community repository, and that installer is unsigned. Users who install through the Microsoft Store, or through `winget --source msstore`, get the Store package that Microsoft signs during certification instead. Publishing to the community source therefore adds a second, unsigned route alongside the signed one — worth doing deliberately rather than by flipping a variable. + ## Automatic `GITHUB_TOKEN` GitHub supplies `GITHUB_TOKEN` per run. Workflows use it for semantic PR validation, release-asset reads, issue bookkeeping, and the Nix bump PR. Its scopes come from each workflow's `permissions` block and it is not manually created or rotated. Do not replace it with a PAT unless cross-workflow triggering or external-repository access is actually required. From 6148d1396b813d8b25c893fbbbae7ebef6a0044c Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Mon, 10 Aug 2026 18:03:25 +0200 Subject: [PATCH 2/3] docs(dist): describe the winget gate by its prerequisites, not its if #329 moves the configuration test out of the job-level if, so "gates on vars.WINGET_IDENTIFIER" stops being true the moment both land. Describe what has to be true instead of the mechanism that checks it, which also lets the paragraph name the two prerequisites nobody could guess: the PAT must be classic, and the action refuses to author a package that does not already exist in winget-pkgs. --- technical-documentation/engineering/release-and-secrets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/technical-documentation/engineering/release-and-secrets.md b/technical-documentation/engineering/release-and-secrets.md index c76712209..4df3c7296 100644 --- a/technical-documentation/engineering/release-and-secrets.md +++ b/technical-documentation/engineering/release-and-secrets.md @@ -147,7 +147,7 @@ The bot token comes from a Discord application authorized with the `bot` scope. `bump-nix-package.yml` uses the workflow-scoped `GITHUB_TOKEN`; it requires repository contents and pull-request write permissions as declared in the workflow and has no additional long-lived secret. -**WinGet publishing is built but switched off.** `publish-winget.yml` gates on `vars.WINGET_IDENTIFIER != ''`, and neither that variable nor `WINGET_ACC_TOKEN` is set, so the job has reported `skipped` on every release so far — including stable ones. Nothing is broken; it has simply never run. Setting both turns it on for the next stable tag with no code change. +**WinGet publishing is built but switched off.** Nothing is broken; it has simply never run, on any release including the stable ones. Four things have to be true before it can, and `publish-winget.yml` names all four in the warning it now emits when it cannot publish: the `WINGET_IDENTIFIER` variable (set, `OpenScreen.OpenScreen`); the `WINGET_ACC_TOKEN` secret (absent — and it must be a *classic* PAT with `public_repo`, since `winget-releaser` does not support fine-grained ones); a fork of `microsoft/winget-pkgs` under `getopenscreen`, which is where the action pushes its branch; and at least one version of the package already merged into `winget-pkgs`, because the action writes each manifest from the previous one and refuses to author the first. That first submission is manual, via `wingetcreate new`. Note what it would publish before turning it on: `winget-releaser` submits the **NSIS `.exe`** attached to the release to the community repository, and that installer is unsigned. Users who install through the Microsoft Store, or through `winget --source msstore`, get the Store package that Microsoft signs during certification instead. Publishing to the community source therefore adds a second, unsigned route alongside the signed one — worth doing deliberately rather than by flipping a variable. From ccfb783a88aadb07cf755cc6877921fafd7d6754 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Mon, 10 Aug 2026 18:16:55 +0200 Subject: [PATCH 3/3] docs(dist): stop calling the winget job switched off CodeRabbit, correctly, on my own paragraph. After #329 the job is not off: it starts on every stable release and either publishes or annotates the run. What is unfinished is the publishing, not the workflow, and describing it as "never run" would be false the moment both land. Keeps the history, which is the part worth remembering: the silent skip is why #148 sat open through eight green releases. --- technical-documentation/engineering/release-and-secrets.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/technical-documentation/engineering/release-and-secrets.md b/technical-documentation/engineering/release-and-secrets.md index 4df3c7296..4adac7d4d 100644 --- a/technical-documentation/engineering/release-and-secrets.md +++ b/technical-documentation/engineering/release-and-secrets.md @@ -147,7 +147,7 @@ The bot token comes from a Discord application authorized with the `bot` scope. `bump-nix-package.yml` uses the workflow-scoped `GITHUB_TOKEN`; it requires repository contents and pull-request write permissions as declared in the workflow and has no additional long-lived secret. -**WinGet publishing is built but switched off.** Nothing is broken; it has simply never run, on any release including the stable ones. Four things have to be true before it can, and `publish-winget.yml` names all four in the warning it now emits when it cannot publish: the `WINGET_IDENTIFIER` variable (set, `OpenScreen.OpenScreen`); the `WINGET_ACC_TOKEN` secret (absent — and it must be a *classic* PAT with `public_repo`, since `winget-releaser` does not support fine-grained ones); a fork of `microsoft/winget-pkgs` under `getopenscreen`, which is where the action pushes its branch; and at least one version of the package already merged into `winget-pkgs`, because the action writes each manifest from the previous one and refuses to author the first. That first submission is manual, via `wingetcreate new`. +**WinGet publishing does not complete yet, and now says so.** `publish-winget.yml` starts on every stable release; whether it publishes depends on four prerequisites, and it names the missing ones in a `::warning::` instead of passing quietly. It used to pass quietly: the configuration test sat in the job-level `if`, an unconfigured job resolved to `skipped`, and a skipped job is green — so eight releases in a row reported success while publishing nothing, which is how #148 stayed open without anyone noticing. The four are: `WINGET_IDENTIFIER` (set, `OpenScreen.OpenScreen`); `WINGET_ACC_TOKEN` (absent — it must be a *classic* PAT with `public_repo`, since `winget-releaser` does not support fine-grained ones); a fork of `microsoft/winget-pkgs` under `getopenscreen`, which is where the action pushes its branch; and at least one version of the package already merged into `winget-pkgs`, because the action writes each manifest from the previous one and refuses to author the first. That first submission is manual, via `wingetcreate new`. Note what it would publish before turning it on: `winget-releaser` submits the **NSIS `.exe`** attached to the release to the community repository, and that installer is unsigned. Users who install through the Microsoft Store, or through `winget --source msstore`, get the Store package that Microsoft signs during certification instead. Publishing to the community source therefore adds a second, unsigned route alongside the signed one — worth doing deliberately rather than by flipping a variable.