What goes wrong
GitHub's publish dialog offers three states: set as the latest release, set as a pre-release, or neither. The in-app updater treats that third state, "none", exactly like latest, so a release published without ticking anything is pushed to every install.
Why
checkForUpdate in static/js/catalog.js fetches the ten newest releases and takes the first one that passes:
releases.find((r) => !r.draft && !r.prerelease)
Two flags, and latest is not one of them. A release published with neither box ticked comes back as draft: false, prerelease: false, matches, and is offered.
Why it matters more than it looks
The release runbook says promotion, "Set as the latest release", is the moment a build becomes visible to the updater. That is the wrong mental model, and it is the one written down. What the updater actually watches is the absence of the pre-release flag. The two agree as long as every release is published as a pre-release first and promoted afterwards, and they disagree the moment anyone publishes a release as "none" believing it is invisible.
So this is not a fault anyone would hit by following the process. It is a fault that turns one slip in the publish dialog into a release shipped to everybody.
What it should do
Offer a release only when GitHub itself marks it as the latest one. Pre-release and "none" should both be silent.
Constraints for whoever fixes it
/releases/latest is the endpoint that answers this: it returns exactly the release GitHub considers latest and excludes drafts and pre-releases by definition. The current code deliberately avoids it, with a comment explaining that the choice belongs in code rather than in GitHub's endpoint semantics. That reasoning is what changes here: "whatever GitHub calls latest" becomes the rule we actually want.
- The REST list endpoint does not expose a per-release "is latest" flag, so filtering the list cannot answer this. It has to be the other endpoint.
stubUpdateCheck in tests/e2e/helpers.mjs stubs an array and its comment states the app polls the list rather than /releases/latest. That stub and its comment both change with this.
- Worth keeping a defensive
!draft && !prerelease check even against that endpoint, so the app does not depend solely on the endpoint's behaviour staying as documented.
What goes wrong
GitHub's publish dialog offers three states: set as the latest release, set as a pre-release, or neither. The in-app updater treats that third state, "none", exactly like latest, so a release published without ticking anything is pushed to every install.
Why
checkForUpdateinstatic/js/catalog.jsfetches the ten newest releases and takes the first one that passes:Two flags, and
latestis not one of them. A release published with neither box ticked comes back asdraft: false, prerelease: false, matches, and is offered.Why it matters more than it looks
The release runbook says promotion, "Set as the latest release", is the moment a build becomes visible to the updater. That is the wrong mental model, and it is the one written down. What the updater actually watches is the absence of the pre-release flag. The two agree as long as every release is published as a pre-release first and promoted afterwards, and they disagree the moment anyone publishes a release as "none" believing it is invisible.
So this is not a fault anyone would hit by following the process. It is a fault that turns one slip in the publish dialog into a release shipped to everybody.
What it should do
Offer a release only when GitHub itself marks it as the latest one. Pre-release and "none" should both be silent.
Constraints for whoever fixes it
/releases/latestis the endpoint that answers this: it returns exactly the release GitHub considers latest and excludes drafts and pre-releases by definition. The current code deliberately avoids it, with a comment explaining that the choice belongs in code rather than in GitHub's endpoint semantics. That reasoning is what changes here: "whatever GitHub calls latest" becomes the rule we actually want.stubUpdateCheckintests/e2e/helpers.mjsstubs an array and its comment states the app polls the list rather than/releases/latest. That stub and its comment both change with this.!draft && !prereleasecheck even against that endpoint, so the app does not depend solely on the endpoint's behaviour staying as documented.